Fixes #60 by documenting/testing simpler :schema format

This commit is contained in:
Sean Corfield 2019-09-09 16:22:25 -07:00
parent 7a110f15ce
commit 2c51c549d6
3 changed files with 36 additions and 10 deletions

View file

@ -11,7 +11,7 @@ The following changes have been committed to the **master** branch since the 1.0
The following changes have been committed to the **issue-60** branch since the 1.0.6 release:
* Address #60 by allowing for additional schema entry formats: `:table/column` is equivalent to the old `[:table :column :one]` and `[:table/column]` is equivalent to the old `[:table :column :many]`. I'm still evaluating what might be needed for #61 before settling on a suitable format for schema extensions.
* 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

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

@ -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]}})