Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
1 contributor

Users who have contributed to this file

162 lines (139 sloc) 7.41 KB
;; Copyright (c) 2018 Sean Corfield. All rights reserved.
;; The use and distribution terms for this software are covered by
;; the Eclipse Public License 1.0
;; (http://opensource.org/licenses/eclipse-1.0.php) which can be
;; found in the file epl-v10.html at the root of this distribution.
;; By using this software in any fashion, you are agreeing to be
;; bound by the terms of this license. You must not remove this
;; notice, or any other, from this software.
;;
;; datafy.clj
;;
;; Exploring datafy and nav for JDBC databases
(ns
^{:author "Sean Corfield",
:doc "Variants of 'query' functions from clojure.java.jdbc that support
the new clojure.datafy functionality in Clojure 1.10.
The whole schema/column lookup piece is very likely to change!
Currently, the :schema option for a 'query' function is a mapping
from column name to a tuple of table name, key column, and optionally
the cardinality (:one -- the default -- or :many). The cardinality
determines whether navigation should produce a single row (hash map)
or a result set.
One of the problems is that the general case -- query -- doesn't
have any concept of an associated table name (and may of course
join across multiple tables), so there's no good way to take the
table name into account when mapping a column to another table.
For find-by-keys and get-by-id, you do have the starting table
name so you could map [table1 column1] to [table2 column2] and have
table-specific mappings.
The obvious, logical thing would be to use SQL metadata to figure
out actual foreign key constraints but not everyone uses them, for
a variety of reasons. For folks who do use them, they can build
their schema structure from the database, and pass the relevant
part of it to the functions below (via :schema in options)."}
clojure.java.jdbc.datafy
(:require [clojure.core.protocols :as p]
[clojure.java.jdbc :as jdbc]))
(declare datafy-result-set)
(declare datafy-row)
(defn- default-schema
"The default schema lookup rule for column names.
If a column name ends with _id or id, it is assumed to be a foreign key
into the table identified by the first part of the column name."
[col]
(let [[_ table] (re-find #"^(.*?)_?id$" (name col))]
(when table
[(keyword table) :id])))
(defn- schema-opt
"Returns the schema 'option'.
As with other clojure.java.jdbc options, it can be provided in the db-spec
hash map or in the options for a particular function call.
A schema can a simple map from column names to pairs of table name and
the key column to be used in that table. A schema can also be a function
that is called with column names and should return nil if the column is
not to be treated as a foreign key, or a pair of table name and the key
column within that table."
[db-spec opts]
(:schema (merge {:schema default-schema}
(when (map? db-spec) db-spec)
opts)))
(defn- navize-row [db-spec opts row]
"Given a db-spec, a map of options, and a row -- a hash map -- return the
row with metadata that provides navigation via foreign keys."
(let [schema (schema-opt db-spec opts)]
(with-meta row
{`p/nav (fn [coll k v]
(let [[table fk cardinality] (schema k)]
(if fk
(try
(if (= :many cardinality)
(datafy-result-set db-spec opts
(jdbc/find-by-keys db-spec table {fk v}))
(datafy-row db-spec opts
(jdbc/get-by-id db-spec table v fk)))
(catch Exception _
;; assume an exception means we just cannot
;; navigate anywhere, so return just the value
v))
v)))})))
(defn- datafy-row [db-spec opts row]
"Given a db-spec, a map of options, and a row -- a hash map -- return the
row with metadata that provides datafication (which in turn provides).
navigation)."
(with-meta row {`p/datafy (partial navize-row db-spec opts)}))
(defn- datafy-result-set [db-spec opts rs]
"Given a db-spec, a map of options, and a result set -- a sequence of hash
maps that represent rows -- return a sequence of datafiable rows."
(mapv (partial datafy-row db-spec opts) rs))
(defn get-by-id
"Given a database connection, a table name, a primary key value, an
optional primary key column name, and an optional options map, return
a single matching row, or nil.
The primary key column name defaults to :id."
([db table pk-value] (get-by-id db table pk-value :id {}))
([db table pk-value pk-name-or-opts]
(if (map? pk-name-or-opts)
(get-by-id db table pk-value :id pk-name-or-opts)
(get-by-id db table pk-value pk-name-or-opts {})))
([db table pk-value pk-name opts]
(datafy-row db opts
(jdbc/get-by-id db table pk-value pk-name opts))))
(defn find-by-keys
"Given a database connection, a table name, a map of column name/value
pairs, and an optional options map, return any matching rows.
An :order-by option may be supplied to sort the rows, e.g.,
{:order-by [{:name :asc} {:age :desc} {:income :asc}]}
;; equivalent to:
{:order-by [:name {:age :desc} :income]}
The :order-by value is a sequence of column names (to sort in ascending
order) and/or maps from column names to directions (:asc or :desc). The
directions may be strings or keywords and are not case-sensitive. They
are mapped to ASC or DESC in the generated SQL.
Note: if a ordering map has more than one key, the order of the columns
in the generated SQL ORDER BY clause is unspecified (so such maps should
only contain one key/value pair)."
([db table columns] (find-by-keys db table columns {}))
([db table columns opts]
(datafy-result-set db opts
(jdbc/find-by-keys db table columns opts))))
(defn query
"Given a database connection and a vector containing SQL and optional parameters,
perform a simple database query. The options specify how to construct the result
set (and are also passed to prepare-statement as needed):
:as-arrays? - return the results as a set of arrays, default false.
:identifiers - applied to each column name in the result set, default lower-case
:keywordize? - defaults to true, can be false to opt-out of converting
identifiers to keywords
:qualifier - optionally provides the namespace qualifier for identifiers
:result-set-fn - applied to the entire result set, default doall / vec
if :as-arrays? true, :result-set-fn will default to vec
if :as-arrays? false, :result-set-fn will default to doall
:row-fn - applied to each row as the result set is constructed, default identity
The second argument is a vector containing a SQL string or PreparedStatement, followed
by any parameters it needs.
See also prepare-statement for additional options."
([db sql-params] (query db sql-params {}))
([db sql-params opts]
(datafy-result-set db opts
(jdbc/query db sql-params opts))))
You can’t perform that action at this time.