Document Result Set Builders
And sketch out the remaining documentation outline.
This commit is contained in:
parent
954ef5ad47
commit
2ab35132a7
10 changed files with 136 additions and 25 deletions
|
|
@ -1,4 +1,8 @@
|
|||
{:cljdoc.doc/tree [["Readme" {:file "README.md"}]
|
||||
["Getting Started" {:file "doc/getting-started.md"}
|
||||
["Friendly SQL Functions" {:file "doc/friendly_sql_fns.md"}]]
|
||||
["Friendly SQL Functions" {:file "doc/friendly_sql_fns.md"}]
|
||||
["Result Set Builders" {:file "doc/rs_builders.md"}]
|
||||
["Prepared Statements" {:file "doc/prepared_stmt.md"}]
|
||||
["Transactions" {:file "doc/transactions.md"}]]
|
||||
["All The Options" {:file "doc/options.md"}]
|
||||
["Migration from clojure.java.jdbc" {:file "doc/differences.md"}]]}
|
||||
|
|
|
|||
1
doc/differences.md
Normal file
1
doc/differences.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Migrating from `clojure.java.jdbc`
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
# Friendly SQL Functions
|
||||
|
||||
In [[Getting Started|getting_started]], we used `execute!` and `execute-one!` for all our SQL operations, except when we are reducing a result set. These functions (and `reducible!`) all expect a vector containing a SQL string followed by any parameter values required.
|
||||
In [[Getting Started|getting_started]], we used `execute!` and `execute-one!` for all our SQL operations, except when we were reducing a result set. These functions (and `reducible!`) all expect a "connectable" and a vector containing a SQL string followed by any parameter values required.
|
||||
|
||||
A "connectable" can be a `javax.sql.DataSource`, a `java.sql.Connection`, or something that can produce a datasource (when `get-datasource` is called on it). It can also be a `java.sql.PreparedStatement` but we'll cover that a bit later...
|
||||
|
||||
Because string-building isn't always much fun, `next.jdbc.sql` also provides some "friendly" functions for basic CRUD operations:
|
||||
|
||||
|
|
@ -11,7 +13,7 @@ Because string-building isn't always much fun, `next.jdbc.sql` also provides som
|
|||
|
||||
as well as these more specific "read" operations:
|
||||
|
||||
* `find-by-keys` -- a query on or more column values, specified as a hash map,
|
||||
* `find-by-keys` -- a query on one or more column values, specified as a hash map,
|
||||
* `get-by-id` -- a query to return a single row, based on a single column value, usually the primary key.
|
||||
|
||||
These functions are described in more detail below. They are intended to cover the most common, simple SQL operations. If you need more expressiveness, consider one of the following libraries to build SQL/parameter vectors, or run queries:
|
||||
|
|
@ -27,10 +29,8 @@ Given a table name (as a keyword) and a hash map of column names and values, thi
|
|||
```clojure
|
||||
(sql/insert! ds :address {:name "A. Person" :email "albert@person.org"})`
|
||||
;; equivalent to
|
||||
(jdbc/execute-one! ds ["
|
||||
INSERT INTO address (name,email)
|
||||
VALUES (?,?)
|
||||
" "A.Person" "albert@person.org"] {:return-keys true})
|
||||
(jdbc/execute-one! ds ["INSERT INTO address (name,email) VALUES (?,?)"
|
||||
"A.Person" "albert@person.org"] {:return-keys true})
|
||||
```
|
||||
|
||||
## `insert-multi!`
|
||||
|
|
@ -44,10 +44,8 @@ Given a table name (as a keyword), a vector of column names, and a vector row va
|
|||
["Waldo" "waldo@lagunitas.beer"]
|
||||
["Aunt Sally" "sour@lagunitas.beer"]])`
|
||||
;; equivalent to
|
||||
(jdbc/execute! ds ["
|
||||
INSERT INTO address (name,email)
|
||||
VALUES (?,?), (?,?), (?,?)
|
||||
" "Stella" "stella@artois.beer"
|
||||
(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})
|
||||
```
|
||||
|
|
@ -140,3 +138,5 @@ These quoting functions can be provided to any of the friendly SQL functions abo
|
|||
```
|
||||
|
||||
Note that the entity naming function is passed a string, the result of calling `name` on the keyword passed in. Also note that the default quoting functions do not handle schema-qualified names, such as `dbo.table_name` -- `sql-server` would produce `[dbo.table_name]` from that [Issue 11](https://github.com/seancorfield/next-jdbc/issues/11).
|
||||
|
||||
[[Prev: Getting Started|getting_started]] [[Next: Row and Result Set Builders|rs_builders]]
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ for `deps.edn` or:
|
|||
```
|
||||
for `project.clj` or `build.boot`.
|
||||
|
||||
In addition, you will need to add dependencies for the JDBC drivers you wish to use for whatever databases you are using. You can the drivers and versions that `next.jdbc` is tested against in [the project's `deps.edn` file](https://github.com/seancorfield/next-jdbc/blob/master/deps.edn#L6-L16), but many other JDBC drivers for other databases should also work (e.g., Oracle, Red Shift).
|
||||
In addition, you will need to add dependencies for the JDBC drivers you wish to use for whatever databases you are using. You can see the drivers and versions that `next.jdbc` is tested against in [the project's `deps.edn` file](https://github.com/seancorfield/next-jdbc/blob/master/deps.edn#L6-L16), but many other JDBC drivers for other databases should also work (e.g., Oracle, Red Shift).
|
||||
|
||||
## An Example REPL Session
|
||||
|
||||
|
|
@ -135,3 +135,5 @@ If `with-transaction` is given a datasource, it will create and close the connec
|
|||
(into [] (map :column) (jdbc/reducible! tx ...)))
|
||||
(jdbc/execute! con ...)) ; committed
|
||||
```
|
||||
|
||||
[[Next: Friendly SQL Functions|friendly_sql_fns]]
|
||||
|
|
|
|||
3
doc/options.md
Normal file
3
doc/options.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# `next.jbc` Options
|
||||
|
||||
[[Prev: Transactions|transactions]] [[Next: Migration from `clojure.java.jdbc`|differences]]
|
||||
3
doc/prepared_stmt.md
Normal file
3
doc/prepared_stmt.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Prepared Statements
|
||||
|
||||
[[Prev: Row and Result Set Builders|rs_builders]] [[Next: Transactions|transactions]]
|
||||
53
doc/rs_builders.md
Normal file
53
doc/rs_builders.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# RowBuilder and ResultSetBuilder
|
||||
|
||||
In [[Getting Started|getting_started]], it was noted that, by default, `execute!` and `execute-one!` return result sets as (vectors of) hash maps with namespace-qualified keys as-is. If your database naturally produces uppercase column names from the JDBC driver, that's what you'll get. If it produces mixed-case names, that's what you'll get.
|
||||
|
||||
The default builder for rows and result sets creates qualified keywords that match whatever case the JDBC driver produces. That builder is `next.jdbc.result-set/as-maps` but there are several options available:
|
||||
|
||||
* `as-maps` -- table-qualified keywords as-is, the default, e.g., `:ADDRESS/ID`, `:myTable/firstName`,
|
||||
* `as-unqualified-maps` -- simple keywords as-is, e.g., `:ID`, `:firstName`,
|
||||
* `as-lower-maps` -- table-qualified lower-case keywords, e.g., `:address/id`, `:mytable/firstname`,
|
||||
* `as-unqualified-lower-maps` -- simple lower-case keywords as-is, e.g., `:id`, `:firstname`,
|
||||
* `as-arrays` -- table-qualified keywords as-is (vector of column names, followed by vectors of row values),
|
||||
* `as-unqualified-arrays` -- simple keywords as-is,
|
||||
* `as-lower-arrays` -- table-qualified lower-case keywords,
|
||||
* `as-unqualified-lower-arrays` -- simple lower-case keywords.
|
||||
|
||||
The reason behind the default is to a) be a simple transform, b) produce qualified keys in keeping with Clojure's direction (with `clojure.spec` etc), and c) not mess with the data. `as-arrays` is (slightly) faster than `as-maps` since it produces less data (vectors of values instead of vectors of hash maps), but the `lower` options will be slightly slower since they include (conditional) logic to convert strings to lower-case. The `unqualified` options may be slightly faster than their qualified equivalents but make no attempt to keep column names unique if your SQL joins across multiple tables.
|
||||
|
||||
## RowBuilder Protocol
|
||||
|
||||
This protocol defines four functions and is used whenever `next.jdbc` needs to materialize a row from a `ResultSet` as a Clojure data structure:
|
||||
|
||||
* `(->row builder)` -- produces a new row (a `(transient {})` by default),
|
||||
* `(column-count builder)` -- returns the number of columns in each row,
|
||||
* `(with-column builder row i)` -- given the row so far, fetches column `i` from the current row of the `ResultSet`, converts it to a Clojure value, and adds it to the row (for `as-maps` this is a call to `.getObject`, a call to `read-column-by-index` -- see the `ReadableColumn` protocol below, and a call to `assoc!`),
|
||||
* `(row! builder row)` -- completes the row (a `(persistent! row)` call by default).
|
||||
|
||||
## ResulSet Protocol
|
||||
|
||||
This protocol defines three functions and is used whenever `next.jdbc` needs to materialize a result set (multiple rows) from a `ResultSet` as a Clojure data structure:
|
||||
|
||||
* `(->rs builder)` -- produces a new result set (a `(transient [])` by default),
|
||||
* `(with-row builder rs row)` -- given the result set so far and a new row, returns the updated result set (a `(conj! rs row)` call by default),
|
||||
* `(rs! builder rs)` -- completes the result set (a `(persistent! rs)` call by default).
|
||||
|
||||
## Result Set Builder Functions
|
||||
|
||||
The `as-*` functions described above are all implemented in terms of these protocols. They are passed the `ResultSet` object and the options hash map (as passed into various `next.jdbc` functions). They return an implementation of the protocols that is then used to build rows and the result set. Note that the `ResultSet` passed in is _mutable_ and is advanced from row to row by the SQL execution function, so each time `->row` is called, the underlying `ResultSet` object points at each new row in turn. By contrast, `->rs` (which is only called by `execute-all!`) is invoked _before_ the `ResultSet` is advanced to the first row.
|
||||
|
||||
The options hash map for any `next.jdbc` function can contain a `:gen-fn` key and the value is used at the row/result set builder function. The tests for `next.jdbc.result-set` include a [record-based builder function](https://github.com/seancorfield/next-jdbc/blob/master/test/next/jdbc/result_set_test.clj#L148-L164) as an example of how you can extend this to satisfy your needs.
|
||||
|
||||
# ReadableColumn
|
||||
|
||||
As mentioned above, when `with-column` is called, the expectation is that the row builder will call `.getObject` on the current state of the `ResultSet` object with the column index and will then call `read-column-by-index`, passing the column value, the `ResultSetMetaData`, and the column index. That function is part of the `ReadableColumn` protocol that you can extend to handle conversion of arbitrary database-specific types to Clojure values.
|
||||
|
||||
In addition, inside `reducible!`, as each value is looked up by name in the current state of the `ResultSet` object, the `read-column-by-label` function is called, again passing the column value and the column label (the name used in the SQL to identify that column). This function is also part of the `ReadableColumn` protocol.
|
||||
|
||||
The default implementation of this protocol is for these two functions to return `nil` as `nil`, a `Boolean` value as a canonical `true` or `false` value (unfortunately, JDBC drivers cannot be relied on to return unique values here!), and for all other objects to be returned as-is.
|
||||
|
||||
Common extensions here could include converting `java.sql.Timestamp` to `java.time.Instant` for example but `next.jdbc` makes no assumptions beyond `nil` and `Boolean`.
|
||||
|
||||
Note that the converse, converting Clojure values to database-specific types is handled by the `SettableParameters`, discussed in the section on [[prepared statements|prepared_stmt]]
|
||||
|
||||
[[Prev: Friendly SQL Functions|friendly_sql_fns]] [[Next: Prepared Statements|prepared_stmt]]
|
||||
3
doc/transactions.md
Normal file
3
doc/transactions.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Transactions
|
||||
|
||||
[[Prev: Prepared Statements|prepared_stmt]] [[Next: All The Options|options]]
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
Also provides the default implemenations for Executable and
|
||||
the default datafy/nav behavior for rows from a result set."
|
||||
(:require [clojure.core.protocols :as core-p]
|
||||
[clojure.string :as str]
|
||||
[next.jdbc.prepare :as prepare]
|
||||
[next.jdbc.protocols :as p])
|
||||
(:import (java.sql PreparedStatement
|
||||
|
|
@ -34,6 +35,23 @@
|
|||
(mapv (fn [^Integer i] (keyword (.getColumnLabel rsmeta i)))
|
||||
(range 1 (inc (.getColumnCount rsmeta)))))
|
||||
|
||||
(defn get-lower-column-names
|
||||
"Given ResultSetMetaData, return a vector of lower-case column names, each
|
||||
qualified by the table from which it came."
|
||||
[^ResultSetMetaData rsmeta opts]
|
||||
(mapv (fn [^Integer i] (keyword (some-> (.getTableName rsmeta i)
|
||||
(not-empty)
|
||||
(str/lower-case))
|
||||
(-> (.getColumnLabel rsmeta i)
|
||||
(str/lower-case))))
|
||||
(range 1 (inc (.getColumnCount rsmeta)))))
|
||||
|
||||
(defn get-unqualified-lower-column-names
|
||||
"Given ResultSetMetaData, return a vector of unqualified column names."
|
||||
[^ResultSetMetaData rsmeta opts]
|
||||
(mapv (fn [^Integer i] (keyword (str/lower-case (.getColumnLabel rsmeta i))))
|
||||
(range 1 (inc (.getColumnCount rsmeta)))))
|
||||
|
||||
(defprotocol ReadableColumn
|
||||
"Protocol for reading objects from the java.sql.ResultSet. Default
|
||||
implementations (for Object and nil) return the argument, and the
|
||||
|
|
@ -114,6 +132,22 @@
|
|||
cols (get-unqualified-column-names rsmeta opts)]
|
||||
(->MapResultSetBuilder rs rsmeta cols)))
|
||||
|
||||
(defn as-lower-maps
|
||||
"Given a ResultSet and options, return a RowBuilder / ResultSetBuilder
|
||||
that produces bare vectors of hash map rows, with lower-case keys."
|
||||
[^ResultSet rs opts]
|
||||
(let [rsmeta (.getMetaData rs)
|
||||
cols (get-lower-column-names rsmeta opts)]
|
||||
(->MapResultSetBuilder rs rsmeta cols)))
|
||||
|
||||
(defn as-unqualified-lower-maps
|
||||
"Given a ResultSet and options, return a RowBuilder / ResultSetBuilder
|
||||
that produces bare vectors of hash map rows, with simple, lower-case keys."
|
||||
[^ResultSet rs opts]
|
||||
(let [rsmeta (.getMetaData rs)
|
||||
cols (get-unqualified-lower-column-names rsmeta opts)]
|
||||
(->MapResultSetBuilder rs rsmeta cols)))
|
||||
|
||||
(defrecord ArrayResultSetBuilder [^ResultSet rs rsmeta cols]
|
||||
RowBuilder
|
||||
(->row [this] (transient []))
|
||||
|
|
@ -144,6 +178,24 @@
|
|||
cols (get-unqualified-column-names rsmeta opts)]
|
||||
(->ArrayResultSetBuilder rs rsmeta cols)))
|
||||
|
||||
(defn as-lower-arrays
|
||||
"Given a ResultSet and options, return a RowBuilder / ResultSetBuilder
|
||||
that produces a vector of lower-case column names followed by vectors of
|
||||
row values."
|
||||
[^ResultSet rs opts]
|
||||
(let [rsmeta (.getMetaData rs)
|
||||
cols (get-lower-column-names rsmeta opts)]
|
||||
(->ArrayResultSetBuilder rs rsmeta cols)))
|
||||
|
||||
(defn as-unqualified-lower-arrays
|
||||
"Given a ResultSet and options, return a RowBuilder / ResultSetBuilder
|
||||
that produces a vector of simple, lower-case column names followed by
|
||||
vectors of row values."
|
||||
[^ResultSet rs opts]
|
||||
(let [rsmeta (.getMetaData rs)
|
||||
cols (get-unqualified-lower-column-names rsmeta opts)]
|
||||
(->ArrayResultSetBuilder rs rsmeta cols)))
|
||||
|
||||
(declare navize-row)
|
||||
|
||||
(defprotocol DatafiableRow
|
||||
|
|
|
|||
|
|
@ -57,16 +57,6 @@
|
|||
(is (= 3 (:FRUIT/ID (first object))))
|
||||
(is (= "Peach" (:FRUIT/NAME (first object))))))))
|
||||
|
||||
(defn lower-case-cols [^ResultSetMetaData rsmeta opts]
|
||||
(mapv (fn [^Integer i]
|
||||
(keyword (str/lower-case (.getColumnLabel rsmeta i))))
|
||||
(range 1 (inc (.getColumnCount rsmeta)))))
|
||||
|
||||
(defn as-lower-case [^ResultSet rs opts]
|
||||
(let [rsmeta (.getMetaData rs)
|
||||
cols (lower-case-cols rsmeta opts)]
|
||||
(rs/->MapResultSetBuilder rs rsmeta cols)))
|
||||
|
||||
(deftest test-map-row-builder
|
||||
(testing "default row builder"
|
||||
(let [row (p/-execute-one (ds)
|
||||
|
|
@ -93,7 +83,7 @@
|
|||
(testing "lower-case row builder"
|
||||
(let [row (p/-execute-one (ds)
|
||||
["select * from fruit where id = ?" 3]
|
||||
{:gen-fn as-lower-case})]
|
||||
{:gen-fn rs/as-lower-maps})]
|
||||
(is (map? row))
|
||||
(is (= 3 (:id row)))
|
||||
(is (= "Peach" (:name row))))))
|
||||
|
|
|
|||
Loading…
Reference in a new issue