diff --git a/CHANGELOG.md b/CHANGELOG.md index 79acf5a..cbe3b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Only accretive/fixative changes will be made from now on. +* 1.2.next in progress + * Enhance `insert-multi!` to accept a sequence of hash maps and also to support batch execution, via PR [#206](https://github.com/seancorfield/next-jdbc/pull/206) -- [@rschmukler](https://github.com/rschmukler). + * Fix HikariCP pooling example. + * 1.2.780 -- 2022-04-04 * 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. diff --git a/doc/friendly-sql-functions.md b/doc/friendly-sql-functions.md index d0bc223..7cb2786 100644 --- a/doc/friendly-sql-functions.md +++ b/doc/friendly-sql-functions.md @@ -53,6 +53,20 @@ Given a table name (as a keyword), a vector of column names, and a vector of row "Aunt Sally" "sour@lagunitas.beer"] {:return-keys true}) ``` +Given a table name (as a keyword) and a vector of hash maps, this performs a multi-row insertion into the database: + +```clojure +(sql/insert-multi! ds :address + [{:name "Stella", :email "stella@artois.beer"} + {:name "Waldo", :email "waldo@lagunitas.beer"} + {:name "Aunt Sally", :email "sour@lagunitas.beer"}]) +;; equivalent to +(jdbc/execute! ds ["INSERT INTO address (name,email) VALUES (?,?), (?,?), (?,?)" + "Stella" "stella@artois.beer" + "Waldo" "waldo@lagunitas.beer" + "Aunt Sally" "sour@lagunitas.beer"] {:return-keys true}) +``` + > Note: this expands to a single SQL statement with placeholders for every value being inserted -- for large sets of rows, this may exceed the limits on SQL string size and/or number of parameters for your JDBC driver or your @@ -60,7 +74,42 @@ database. Several databases have a limit of 1,000 parameter placeholders. Oracle does not support this form of multi-row insert, requiring a different syntax altogether. -You should look at [`next.jdbc/execute-batch!`](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc#execute-batch!) for an alternative approach. +### Batch Insertion + +As of release 1.2.next, you can specify `:batch true` in the options, which +will use `execute-batch!` under the hood, instead of `execute!`, as follows: + +```clojure +(sql/insert-multi! ds :address + [:name :email] + [["Stella" "stella@artois.beer"] + ["Waldo" "waldo@lagunitas.beer"] + ["Aunt Sally" "sour@lagunitas.beer"]] + {:batch true}) +;; equivalent to +(jdbc/execute-batch! ds + ["INSERT INTO address (name,email) VALUES (?,?)" + ["Stella" "stella@artois.beer"] + ["Waldo" "waldo@lagunitas.beer"] + ["Aunt Sally" "sour@lagunitas.beer"]] + {:return-keys true :return-generated-keys true}) +;; and +(sql/insert-multi! ds :address + [:name :email] + [{:name "Stella", :email "stella@artois.beer"} + {:name "Waldo", :email "waldo@lagunitas.beer"} + {:name "Aunt Sally", :email "sour@lagunitas.beer"}] + {:batch true}) +;; equivalent to +(jdbc/execute-batch! ds + ["INSERT INTO address (name,email) VALUES (?,?)" + ["Stella" "stella@artois.beer"] + ["Waldo" "waldo@lagunitas.beer"] + ["Aunt Sally" "sour@lagunitas.beer"]] + {:return-keys true :return-generated-keys true}) +``` + +See [**Batched Parameters**](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/getting-started/prepared-statements#caveats) for caveats and possible database-specific behaviors. ## `query` diff --git a/src/next/jdbc/sql.clj b/src/next/jdbc/sql.clj index c207fc6..bd40873 100644 --- a/src/next/jdbc/sql.clj +++ b/src/next/jdbc/sql.clj @@ -1,4 +1,4 @@ -;; copyright (c) 2019-2021 Sean Corfield, all rights reserved +;; copyright (c) 2019-2022 Sean Corfield, all rights reserved (ns next.jdbc.sql "Some utility functions that make common operations easier by @@ -43,15 +43,17 @@ (merge {:return-keys true} opts))))) (defn insert-multi! - "Syntactic sugar over `execute!` to make inserting columns/rows easier. + "Syntactic sugar over `execute!` or `execute-batch!` to make inserting + columns/rows easier. Given a connectable object, a table name, a sequence of column names, and a vector of rows of data (vectors of column values), inserts the data as multiple rows in the database and attempts to return a vector of maps of generated keys. - Also supports a sequence of hash maps with keys corresponding to column - names. + Given a connectable object, a table name, a sequence of hash maps of data, + inserts the data as multiple rows in the database and attempts to return + a vector of maps of generated keys. If called with `:batch` true will call `execute-batch!` - see its documentation for situations in which the generated keys may or may not be returned as well as @@ -68,12 +70,15 @@ ([connectable table hash-maps] (insert-multi! connectable table hash-maps {})) ([connectable table hash-maps-or-cols opts-or-rows] - (if-not (-> hash-maps-or-cols first map?) - (insert-multi! connectable table hash-maps-or-cols opts-or-rows {}) + (if (map? (first hash-maps-or-cols)) (let [cols (keys (first hash-maps-or-cols)) ->row (fn ->row [m] - (map (partial get m) cols))] - (insert-multi! connectable table cols (map ->row hash-maps-or-cols) opts-or-rows)))) + (map #(get m %) cols))] + (when-not (apply = (map (comp set keys) hash-maps-or-cols)) + (throw (IllegalArgumentException. + "insert-multi! hash maps must all have the same keys"))) + (insert-multi! connectable table cols (map ->row hash-maps-or-cols) opts-or-rows)) + (insert-multi! connectable table hash-maps-or-cols opts-or-rows {}))) ([connectable table cols rows opts] (if (seq rows) (let [opts (merge (:options connectable) opts) diff --git a/src/next/jdbc/sql/builder.clj b/src/next/jdbc/sql/builder.clj index c4cd813..b067dfa 100644 --- a/src/next/jdbc/sql/builder.clj +++ b/src/next/jdbc/sql/builder.clj @@ -1,4 +1,4 @@ -;; copyright (c) 2019-2021 Sean Corfield, all rights reserved +;; copyright (c) 2019-2022 Sean Corfield, all rights reserved (ns next.jdbc.sql.builder "Some utility functions for building SQL strings. @@ -157,7 +157,9 @@ (into [(str "INSERT INTO " (table-fn (safe-name table)) " (" params ")" " VALUES " - (str/join ", " (repeat (if batch? 1 (count rows)) (str "(" places ")"))) + (if batch? + (str "(" places ")") + (str/join ", " (repeat (count rows) (str "(" places ")")))) (when-let [suffix (:suffix opts)] (str " " suffix)))] (if batch? identity cat) diff --git a/test/next/jdbc/sql_test.clj b/test/next/jdbc/sql_test.clj index c2859a5..2ede278 100644 --- a/test/next/jdbc/sql_test.clj +++ b/test/next/jdbc/sql_test.clj @@ -199,6 +199,10 @@ (is (thrown? clojure.lang.ExceptionInfo (sql/insert-multi! (ds) :fruit [] [[] [] []])))) +(deftest no-mismatched-columns + (is (thrown? IllegalArgumentException + (sql/insert-multi! (ds) :fruit [{:name "Apple"} {:cost 1.23}])))) + (deftest no-empty-order-by (is (thrown? clojure.lang.ExceptionInfo (sql/find-by-keys (ds) :fruit