next-jdbc/src/next/jdbc/prepare.clj

174 lines
6.8 KiB
Clojure
Raw Normal View History

2019-03-31 23:54:34 +00:00
;; copyright (c) 2018-2019 Sean Corfield, all rights reserved
(ns next.jdbc.prepare
2019-07-15 21:09:48 +00:00
"Mostly an implementation namespace for how `PreparedStatement` objects are
2019-04-01 06:17:12 +00:00
created by the next generation java.jdbc library.
2019-04-21 23:13:52 +00:00
`set-parameters` is public and may be useful if you have a `PreparedStatement`
2019-04-18 06:43:32 +00:00
that you wish to reuse and (re)set the parameters on it.
`execute-batch!` provides a way to add batches of parameters to a
`PreparedStatement` and then execute it in batch mode (via `.executeBatch`).
2019-04-21 23:13:52 +00:00
Defines the `SettableParameter` protocol for converting Clojure values
2019-04-18 06:43:32 +00:00
to database-specific values."
2019-03-31 23:54:34 +00:00
(:require [next.jdbc.protocols :as p])
(:import (java.sql Connection
PreparedStatement
ResultSet
Statement)))
(set! *warn-on-reflection* true)
(defprotocol SettableParameter :extend-via-metadata true
"Protocol for setting SQL parameters in statement objects, which
can convert from Clojure values. The default implementation just
calls `.setObject` on the parameter value. It can be extended to
use other methods of `PreparedStatement` to convert and set parameter
values. Extension via metadata is supported."
(set-parameter [val stmt ix]
"Convert a Clojure value into a SQL value and store it as the ix'th
parameter in the given SQL statement object."))
(extend-protocol SettableParameter
Object
(set-parameter [v ^PreparedStatement s ^long i]
(.setObject s i v))
nil
(set-parameter [_ ^PreparedStatement s ^long i]
(.setObject s i nil)))
2019-03-31 23:54:34 +00:00
(defn set-parameters
2019-04-21 23:13:52 +00:00
"Given a `PreparedStatement` and a vector of parameter values, update the
`PreparedStatement` with those parameters and return it."
2019-03-31 23:54:34 +00:00
^java.sql.PreparedStatement
[^PreparedStatement ps params]
(when (seq params)
(loop [[p & more] params i 1]
(set-parameter p ps i)
2019-03-31 23:54:34 +00:00
(when more
(recur more (inc i)))))
ps)
(def ^{:private true
2019-04-21 23:13:52 +00:00
:doc "Map friendly `:concurrency` values to `ResultSet` constants."}
2019-03-31 23:54:34 +00:00
result-set-concurrency
{:read-only ResultSet/CONCUR_READ_ONLY
:updatable ResultSet/CONCUR_UPDATABLE})
(def ^{:private true
2019-04-21 23:13:52 +00:00
:doc "Map friendly `:cursors` values to `ResultSet` constants."}
2019-03-31 23:54:34 +00:00
result-set-holdability
{:hold ResultSet/HOLD_CURSORS_OVER_COMMIT
:close ResultSet/CLOSE_CURSORS_AT_COMMIT})
(def ^{:private true
2019-04-21 23:13:52 +00:00
:doc "Map friendly `:type` values to `ResultSet` constants."}
2019-03-31 23:54:34 +00:00
result-set-type
{:forward-only ResultSet/TYPE_FORWARD_ONLY
:scroll-insensitive ResultSet/TYPE_SCROLL_INSENSITIVE
:scroll-sensitive ResultSet/TYPE_SCROLL_SENSITIVE})
(defn- ^{:tag (class (into-array String []))} string-array
[return-keys]
(into-array String return-keys))
(defn create
"This is an implementation detail -- use `next.jdbc/prepare` instead.
Given a `Connection`, a SQL string, some parameters, and some options,
2019-04-21 23:13:52 +00:00
return a `PreparedStatement` representing that."
^java.sql.PreparedStatement
[^Connection con ^String sql params
{:keys [return-keys result-type concurrency cursors
2019-03-31 23:54:34 +00:00
fetch-size max-rows timeout]}]
(let [^PreparedStatement ps
(cond
return-keys
(do
(when (or result-type concurrency cursors)
(throw (IllegalArgumentException.
(str ":concurrency, :cursors, and :result-type "
"may not be specified with :return-keys."))))
(if (vector? return-keys)
(let [key-names (string-array return-keys)]
2019-03-31 23:54:34 +00:00
(try
(try
(.prepareStatement con sql key-names)
(catch Exception _
;; assume it is unsupported and try regular generated keys:
(.prepareStatement con sql Statement/RETURN_GENERATED_KEYS)))
2019-03-31 23:54:34 +00:00
(catch Exception _
;; assume it is unsupported and try basic PreparedStatement:
(.prepareStatement con sql))))
(try
(.prepareStatement con sql Statement/RETURN_GENERATED_KEYS)
2019-03-31 23:54:34 +00:00
(catch Exception _
;; assume it is unsupported and try basic PreparedStatement:
(.prepareStatement con sql)))))
(and result-type concurrency)
(if cursors
(.prepareStatement con sql
(get result-set-type result-type result-type)
(get result-set-concurrency concurrency concurrency)
(get result-set-holdability cursors cursors))
(.prepareStatement con sql
(get result-set-type result-type result-type)
(get result-set-concurrency concurrency concurrency)))
(or result-type concurrency cursors)
(throw (IllegalArgumentException.
(str ":concurrency, :cursors, and :result-type "
"may not be specified independently.")))
:else
(.prepareStatement con sql))]
(when fetch-size
(.setFetchSize ps fetch-size))
(when max-rows
(.setMaxRows ps max-rows))
(when timeout
(.setQueryTimeout ps timeout))
(set-parameters ps params)))
2019-03-31 23:54:34 +00:00
(extend-protocol p/Preparable
java.sql.Connection
(prepare [this sql-params opts]
(create this (first sql-params) (rest sql-params) opts)))
(defn execute-batch!
"Given a `PreparedStatement` and a vector containing parameter groups,
i.e., a vector of vector of parameters, use `.addBatch` to add each group
of parameters to the prepared statement and then call `.executeBatch`.
A vector of update counts is returned.
An options hash map may also be provided, containing `:batch-size` which
determines how to partition the parameter groups for submission to the
database. If omitted, all groups will be submitted as a single command.
If you expect the update counts to be larger than `Integer/MAX_VALUE`,
you can specify `:large true` and `.executeLargeBatch` will be called
instead.
Returns a Clojure vector of update counts.
May throw `java.sql.BatchUpdateException` if any part of the batch fails.
You may be able to call `.getUpdateCounts` on that exception object to
get more information about which parts succeeded and which failed."
([ps param-groups]
(execute-batch! ps param-groups {}))
([^PreparedStatement ps param-groups opts]
(let [params (if-let [n (:batch-size opts)]
(if (and (number? n) (pos? n))
(partition-all n param-groups)
(throw (IllegalArgumentException.
":batch-size must be positive")))
[param-groups])]
(into []
(mapcat (fn [group]
(run! #(.addBatch (set-parameters ps %)) group)
(if (:large opts)
(.executeLargeBatch ps)
(.executeBatch ps))))
params))))