Merge pull request #62 from seancorfield/issue-60

Fixes #60 by adopting a simpler :schema format
This commit is contained in:
Sean Corfield 2019-09-09 16:24:35 -07:00 committed by GitHub
commit 30c668c86c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 80 additions and 11 deletions

View file

@ -9,6 +9,10 @@ The following changes have been committed to the **master** branch since the 1.0
* Added test for using `any(?)` and arrays in PostgreSQL for `IN (?,,,?)` style queries. Added a **Tips & Tricks** section to **Friendly SQL Functions** with database-specific suggestions, that starts with this one.
* Improved documentation in several areas.
The following changes have been committed to the **issue-60** branch since the 1.0.6 release:
* Address #60 by supporting simpler schema entry formats: `:table/column` is equivalent to the old `[:table :column :one]` and `[:table/column]` is equivalent to the old `[:table :column :many]`. The older formats will continue to be supported but should be considered deprecated.
## Stable Builds
* 2019-08-24 -- 1.0.6

View file

@ -20,22 +20,24 @@ In addition to `execute!` and `execute-one!`, you can call `next.jdbc.result-set
By default, `next.jdbc` assumes that a column named `<something>id` or `<something>_id` is a foreign key into a table called `<something>` with a primary key called `id`. As an example, if you have a table `address` which has columns `id` (the primary key), `name`, `email`, etc, and a table `contact` which has various columns including `addressid`, then if you retrieve a result set based on `contact`, call `datafy` on it and then "drill down" into the columns, when `(nav row :contact/addressid v)` is called (where `v` is the value of that column in that row) `next.jdbc`'s implementation of `nav` will fetch a single row from the `address` table, identified by `id` matching `v`.
You can override this default behavior for any column in any table by providing a `:schema` option that is a hash map whose keys are column names (usually the table-qualified keywords that `next.jdbc` produces by default) and whose values are tuples containing the name of the table to which that column is a foreign key and the name of the key column within that table. These tuples can optionally include a third value which indicates the cardinality of the foreign key relationship: `:one` or `:many`. The default is `:one` and indicates a one-to-one or many-to-one relationship -- `nav`igation will produce a single row. `:many` indicates a one-to-many or many-to-many relationship -- `nav`igation will produce a result set.
You can override this default behavior for any column in any table by providing a `:schema` option that is a hash map whose keys are column names (usually the table-qualified keywords that `next.jdbc` produces by default) and whose values are table-qualified keywords, optionally wrapped in vectors, that identity the name of the table to which that column is a foreign key and the name of the key column within that table.
The default behavior in the example above is equivalent to this `:schema` value:
```clojure
{:contact/addressid [:address :id :one]} ; :one is the default and could be omitted
{:contact/addressid :address/id} ; a one-to-one or many-to-one relationship
```
If you had a table to track the current valid/bouncing status of email addresses, where `email` is the primary key, you could provide automatic navigation into that using:
If you had a table to track the valid/bouncing status of email addresses over time, `:deliverability`, where `email` is the non-unique key, you could provide automatic navigation into that using:
```clojure
{:contact/addressid [:address :id :one]
:address/email [:deliverability :email]}
{:contact/addressid :address/id
:address/email [:deliverability/email]} ; one-to-many or many-to-many
```
If you use foreign key constraints in your database, you could probably generate this `:schema` data structure automatically from the metadata in your database.
When you indicate a `*-to-many` relationship, by wrapping the foreign table/key in a vector, `next.jdbc`'s implementation of `nav` will fetch a multi-row result set from the target table.
If you use foreign key constraints in your database, you could probably generate this `:schema` data structure automatically from the metadata in your database. Similarly, if you use a library that depends on an entity relationship map (such as [seql](https://exoscale.github.io/seql/) or [walkable](https://walkable.gitlab.io/)), then you could probably generate this `:schema` data structure from that entity map.
## Behind The Scenes

View file

@ -620,6 +620,44 @@
(when table
[(keyword table) :id])))
(defn- expand-schema
"Given a (possibly nil) schema entry, return it expanded to a triple of:
[table fk cardinality]
Possibly schema entry input formats are:
* [table fk] => cardinality :one
* [table fk cardinality] -- no change
* :table/fk => [:table :fk :one]
* [:table/fk] => [:table :fk :many]"
[k entry]
(when entry
(if-let [mapping
(cond
(keyword? entry)
[(keyword (namespace entry)) (keyword (name entry)) :one]
(coll? entry)
(let [[table fk cardinality] entry]
(cond (and table fk cardinality)
entry
(and table fk)
[table fk :one]
(keyword? table)
[(keyword (namespace table)) (keyword (name table)) :many])))]
mapping
(throw (ex-info (str "Invalid schema entry for: " (name k)) {:entry entry})))))
(comment
(expand-schema :user/statusid nil)
(expand-schema :user/statusid :status/id)
(expand-schema :user/statusid [:status :id])
(expand-schema :user/email [:deliverability :email :many])
(expand-schema :user/email [:deliverability/email]))
(defn- navize-row
"Given a connectable object, return a function that knows how to turn a row
into a `nav`igable object.
@ -642,8 +680,9 @@
(with-meta row
{`core-p/nav (fn [coll k v]
(try
(let [[table fk cardinality] (or (get-in opts [:schema k])
(default-schema k))]
(let [[table fk cardinality]
(expand-schema k (or (get-in opts [:schema k])
(default-schema k)))]
(if fk
(let [entity-fn (:table-fn opts identity)
exec-fn! (if (= :many cardinality)

View file

@ -30,7 +30,32 @@
;; check nav produces a single map with the expected key/value data
(is (= 1 ((if (postgres?) :fruit/id :FRUIT/ID) object)))
(is (= "Apple" ((if (postgres?) :fruit/name :FRUIT/NAME) object))))))
(testing "custom schema :one"
(testing "custom schema *-to-1"
(let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 2} connectable
{:schema {:foo/bar :fruit/id}})
data (d/datafy test-row)
v (get data :foo/bar)]
;; check datafication is sane
(is (= 2 v))
(let [object (d/nav data :foo/bar v)]
;; check nav produces a single map with the expected key/value data
(is (= 2 ((if (postgres?) :fruit/id :FRUIT/ID) object)))
(is (= "Banana" ((if (postgres?) :fruit/name :FRUIT/NAME) object))))))
(testing "custom schema *-to-many"
(let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 3} connectable
{:schema {:foo/bar [:fruit/id]}})
data (d/datafy test-row)
v (get data :foo/bar)]
;; check datafication is sane
(is (= 3 v))
(let [object (d/nav data :foo/bar v)]
;; check nav produces a result set with the expected key/value data
(is (vector? object))
(is (= 3 ((if (postgres?) :fruit/id :FRUIT/ID) (first object))))
(is (= "Peach" ((if (postgres?) :fruit/name :FRUIT/NAME) (first object)))))))
(testing "legacy schema tuples"
(let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 2} connectable
{:schema {:foo/bar [:fruit :id]}})
@ -41,8 +66,7 @@
(let [object (d/nav data :foo/bar v)]
;; check nav produces a single map with the expected key/value data
(is (= 2 ((if (postgres?) :fruit/id :FRUIT/ID) object)))
(is (= "Banana" ((if (postgres?) :fruit/name :FRUIT/NAME) object))))))
(testing "custom schema :many"
(is (= "Banana" ((if (postgres?) :fruit/name :FRUIT/NAME) object)))))
(let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 3} connectable
{:schema {:foo/bar [:fruit :id :many]}})