dependencies
| (this space intentionally left almost blank) | ||||||||||||||||||
Helper functions for querying the DB and inserting or updating records using Toucan models. | (ns toucan.db (:refer-clojure :exclude [count]) (:require [clojure [pprint :refer [pprint]] [string :as s] [walk :as walk]] [clojure.java.jdbc :as jdbc] [clojure.tools.logging :as log] [honeysql [core :as hsql] [format :as hformat] [helpers :as h]] [toucan [models :as models] [util :as u]])) | ||||||||||||||||||
CONFIGURATION | |||||||||||||||||||
Quoting Style | |||||||||||||||||||
The quoting style is the HoneySQL quoting style that should be used to quote identifiers. By default, this is
| |||||||||||||||||||
(defonce ^:private default-quoting-style (atom :ansi)) | |||||||||||||||||||
Bind this to override the identifier quoting style. Provided for cases where you want to override the quoting style (such as when connecting to a different DB) without changing the default value. | (def ^:dynamic *quoting-style* nil) | ||||||||||||||||||
Set the default quoting style that should be used to quote identifiers. Defaults to | (defn set-default-quoting-style! [new-quoting-style] (reset! default-quoting-style new-quoting-style)) | ||||||||||||||||||
Fetch the HoneySQL quoting style that should be used to quote identifiers. One of Returns the value of | (defn quoting-style ^clojure.lang.Keyword [] (or *quoting-style* @default-quoting-style)) | ||||||||||||||||||
Additional HoneySQL options | |||||||||||||||||||
Automatically Convert Dashes & Underscores | |||||||||||||||||||
Convert dashes to underscores in queries going into the DB, and underscores in results back to dashes coming out of
the DB. By default, this is disabled. See the documentation in | |||||||||||||||||||
(defonce ^:private default-automatically-convert-dashes-and-underscores (atom false)) | |||||||||||||||||||
Bind this to enable automatic conversion between dashes and underscores for indentifiers. Provided for cases where you want to override the behavior (such as when connecting to a different DB) without changing the default value. | (def ^:dynamic *automatically-convert-dashes-and-underscores* nil) | ||||||||||||||||||
Set the default value for allowing dashes in field names. Defaults to | (defn set-default-automatically-convert-dashes-and-underscores! [^Boolean new-automatically-convert-dashes-and-underscores] (reset! default-automatically-convert-dashes-and-underscores new-automatically-convert-dashes-and-underscores)) | ||||||||||||||||||
Deterimine whether we should automatically convert dashes and underscores in identifiers. Returns the value of | (defn automatically-convert-dashes-and-underscores? ^Boolean [] (if (nil? *automatically-convert-dashes-and-underscores*) @default-automatically-convert-dashes-and-underscores *automatically-convert-dashes-and-underscores*)) | ||||||||||||||||||
DB Connection | |||||||||||||||||||
The default DB connection is used automatically when accessing the DB; it can be anything that would normally be
passed to | |||||||||||||||||||
(defonce ^:private default-db-connection (atom nil)) | |||||||||||||||||||
Bind this to override the default DB connection used by | (def ^:dynamic *db-connection* nil) | ||||||||||||||||||
Set the JDBC connecton details map for the default application DB connection. This connection is used by default by
the various
| (defn set-default-db-connection! {:style/indent 0} [db-connection-map] (reset! default-db-connection db-connection-map)) | ||||||||||||||||||
(defonce ^:private default-jdbc-options ;; FIXME: This has already been fixed in `clojure.java.jdbc`, so ;; this option can be removed when using >= 0.7.10. (atom {:identifiers u/lower-case})) | |||||||||||||||||||
Set the default options to be used for all calls to | (defn set-default-jdbc-options! [jdbc-options] (reset! default-jdbc-options jdbc-options)) | ||||||||||||||||||
TRANSACTION & CONNECTION UTIL FNS | |||||||||||||||||||
Transaction connection to the application DB. Used internally by | (def ^:dynamic *transaction-connection* nil) | ||||||||||||||||||
Fetch the JDBC connection details for passing to If no DB connection has been set this function will throw an exception. | (defn connection [] (or *db-connection* *transaction-connection* @default-db-connection (throw (Exception. "DB is not set up. Make sure to call set-default-db-connection! or bind *db-connection*.")))) | ||||||||||||||||||
Execute F inside a DB transaction. Prefer macro form | (defn do-in-transaction [f] (jdbc/with-db-transaction [conn (connection)] (binding [*transaction-connection* conn] (f)))) | ||||||||||||||||||
Execute all queries within the body in a single transaction. | (defmacro transaction {:arglists '([body] [options & body]), :style/indent 0} [& body] `(do-in-transaction (fn [] ~@body))) | ||||||||||||||||||
QUERY UTIL FNS | |||||||||||||||||||
Should we disable logging for database queries? Normally | (def ^:dynamic ^Boolean *disable-db-logging* false) | ||||||||||||||||||
Return the namespace symbol where we'd expect to find an model symbol.
| (defn- model-symb->ns [symb] {:pre [(symbol? symb)]} (symbol (str (models/root-namespace) \. (u/lower-case (s/replace (name symb) #"([a-z])([A-Z])" "$1-$2"))))) | ||||||||||||||||||
Resolve the model associated with SYMB, calling
| (defn- resolve-model-from-symbol [symb] (let [model-ns (model-symb->ns symb)] @(try (ns-resolve model-ns symb) (catch Throwable _ (require model-ns) (ns-resolve model-ns symb))))) | ||||||||||||||||||
Resolve a model if it's quoted. This also unwraps entities when they're inside vectores.
| (defn resolve-model [model] {:post [(:toucan.models/model %)]} (cond (:toucan.models/model model) model (vector? model) (resolve-model (first model)) (symbol? model) (resolve-model-from-symbol model) :else (throw (Exception. (str "Invalid model: " model))))) | ||||||||||||||||||
The function that JDBC should use to quote identifiers for our database. This is passed as the | (defn quote-fn [] ((quoting-style) @(resolve 'honeysql.format/quote-fns))) ; have to call resolve because it's not public | ||||||||||||||||||
Atom used as a counter for DB calls when enabled. This number isn't perfectly accurate, only mostly; DB calls made directly to JDBC won't be logged. | (def ^:private ^:dynamic *call-count* nil) | ||||||||||||||||||
Execute F with DB call counting enabled. F is passed a single argument, a function that can be used to retrieve the
current call count. (It's probably more useful to use the macro form of this function, | (defn -do-with-call-counting {:style/indent 0} [f] (binding [*call-count* (atom 0)] (f (partial deref *call-count*)))) | ||||||||||||||||||
Execute
| (defmacro with-call-counting {:style/indent 1} [[call-count-fn-binding] & body] `(-do-with-call-counting (fn [~call-count-fn-binding] ~@body))) | ||||||||||||||||||
Print the number of DB calls executed inside | (defmacro debug-count-calls {:style/indent 0} [& body] `(with-call-counting [call-count#] (let [results# (do ~@body)] (println "DB Calls:" (call-count#)) results#))) | ||||||||||||||||||
(defn- format-sql [sql] (when sql (loop [sql sql, [k & more] ["FROM" "LEFT JOIN" "INNER JOIN" "WHERE" "GROUP BY" "HAVING" "ORDER BY" "OFFSET" "LIMIT"]] (if-not k sql (recur (s/replace sql (re-pattern (format "\\s+%s\\s+" k)) (format "\n%s " k)) more))))) | |||||||||||||||||||
(def ^:dynamic ^:private *debug-print-queries* false) | |||||||||||||||||||
Execute | (defn -do-with-debug-print-queries [f] (binding [*debug-print-queries* true] (f))) | ||||||||||||||||||
Print the HoneySQL and SQL forms of any queries executed inside | (defmacro debug-print-queries {:style/indent 0} [& body] `(-do-with-debug-print-queries (fn [] ~@body))) | ||||||||||||||||||
Compile | (defn honeysql->sql [honeysql-form] {:pre [(map? honeysql-form)]} ;; Not sure *why* but without setting this binding on *rare* occasion HoneySQL will unwantedly ;; generate SQL for a subquery and wrap the query in parens like "(UPDATE ...)" which is invalid (let [[sql & args :as sql+args] (binding [hformat/*subquery?* false] (hsql/format honeysql-form :quoting (quoting-style) :allow-dashed-names? (not (automatically-convert-dashes-and-underscores?))))] (when *debug-print-queries* (println (pprint honeysql-form) (format "\n%s\n%s" (format-sql sql) args))) (when-not *disable-db-logging* (log/debug (str "DB Call: " sql)) (when *call-count* (swap! *call-count* inc))) sql+args)) | ||||||||||||||||||
Compile | (defn query [honeysql-form & {:as options}] (jdbc/query (connection) (honeysql->sql honeysql-form) (merge @default-jdbc-options options))) | ||||||||||||||||||
Compile | (defn reducible-query [honeysql-form & {:as options}] (jdbc/reducible-query (connection) (honeysql->sql honeysql-form) options)) | ||||||||||||||||||
Qualify a
| (defn qualify ^clojure.lang.Keyword [model field-name] (if (vector? field-name) [(qualify model (first field-name)) (second field-name)] (hsql/qualify (:table (resolve-model model)) field-name))) | ||||||||||||||||||
Is | (defn qualified? ^Boolean [field-name] (if (vector? field-name) (qualified? (first field-name)) (boolean (re-find #"\." (name field-name))))) | ||||||||||||||||||
Qualify | (defn- maybe-qualify ^clojure.lang.Keyword [model field-name] (if (qualified? field-name) field-name (qualify model field-name))) | ||||||||||||||||||
Get the fields that should be used in a query, destructuring
| (defn- model->fields [model] (if (vector? model) (let [[model & fields] model] (for [field fields] (maybe-qualify model field))) (models/default-fields (resolve-model model)))) | ||||||||||||||||||
Replace underscores in
| (defn- replace-underscores ^clojure.lang.Keyword [k] ;; if k is not a string or keyword don't transform it (if-not ((some-fn string? keyword?) k) k (let [k-str (u/keyword->qualified-name k)] (if (s/index-of k-str \_) (keyword (s/replace k-str \_ \-)) k)))) | ||||||||||||||||||
Replace the keys in any maps in | (defn- transform-keys [f x] (walk/postwalk (fn [y] (if-not (map? y) y (into {} (for [[k v] y] [(f k) v])))) x)) | ||||||||||||||||||
Perform post-processing for objects fetched from the DB. Convert results | (defn do-post-select {:style/indent 1} [model objects] (let [model (resolve-model model) key-transform-fn (if-not (automatically-convert-dashes-and-underscores?) identity (partial transform-keys replace-underscores))] (vec (for [object objects] (models/do-post-select model (key-transform-fn object)))))) | ||||||||||||||||||
Includes projected fields and a from clause for | (defn- merge-select-and-from [resolved-model honeysql-form] (merge {:select (or (models/default-fields resolved-model) [:*]) :from [resolved-model]} honeysql-form)) | ||||||||||||||||||
Select objects from the database. Like
| (defn simple-select {:style/indent 1} [model honeysql-form] (let [model (resolve-model model)] (do-post-select model (query (merge-select-and-from model honeysql-form))))) | ||||||||||||||||||
Select objects from the database. Same as
| (defn simple-select-reducible {:style/indent 1} [model honeysql-form] (let [model (resolve-model model)] (eduction (map #(models/do-post-select model %)) (reducible-query (merge-select-and-from model honeysql-form))))) | ||||||||||||||||||
Select a single object from the database. Like
| (defn simple-select-one ([model] (simple-select-one model {})) ([model honeysql-form] (first (simple-select model (h/limit honeysql-form (hsql/inline 1)))))) | ||||||||||||||||||
Compile | (defn execute! [honeysql-form & {:as options}] (jdbc/execute! (connection) (honeysql->sql honeysql-form) (merge @default-jdbc-options options))) | ||||||||||||||||||
Generate a HoneySQL
| (defn- where {:style/indent 1} ([honeysql-form] honeysql-form) ; no-op ([honeysql-form m] (apply where honeysql-form (apply concat m))) ([honeysql-form k v] (h/merge-where honeysql-form (if (vector? v) (let [[f & args] v] ; e.g. :id [:!= 1] -> [:!= :id 1] (assert (keyword? f)) (vec (cons f (cons k args)))) [:= k v]))) ([honeysql-form k v & more] (apply where (where honeysql-form k v) more))) | ||||||||||||||||||
Generate a HoneySQL form, converting pairs of arguments with keywords into a
| (defn- where+ [honeysql-form args] (loop [honeysql-form honeysql-form, [first-arg & [second-arg & more, :as butfirst]] args] (cond (keyword? first-arg) (recur (where honeysql-form first-arg second-arg) more) first-arg (recur (merge honeysql-form first-arg) butfirst) :else honeysql-form))) | ||||||||||||||||||
UPDATE! | |||||||||||||||||||
(defn- method-implemented? [^clojure.lang.Keyword methodk model] (not (nil? (find-protocol-method models/IModel methodk model)))) | |||||||||||||||||||
Update a single row in the database. Returns
| (defn update! {:style/indent 2} (^Boolean [model honeysql-form] (let [model (resolve-model model)] (not= [0] (execute! (merge (h/update model) honeysql-form))))) (^Boolean [model id kvs] {:pre [(some? id) (map? kvs) (every? keyword? (keys kvs))]} (let [model (resolve-model model) primary-key (models/primary-key model) kvs (-> (models/do-pre-update model (assoc kvs primary-key id)) (dissoc primary-key)) updated? (update! model (-> (h/sset {} kvs) (where primary-key id)))] (when (and updated? (method-implemented? :post-update model)) (models/post-update (model id))) updated?)) (^Boolean [model id k v & more] (update! model id (apply array-map k v more)))) | ||||||||||||||||||
Convenience for updating several objects matching
| (defn update-where! {:style/indent 2} ^Boolean [model conditions-map & {:as values}] {:pre [(map? conditions-map) (every? keyword? (keys values))]} (update! model (where {:set values} conditions-map))) | ||||||||||||||||||
Like | (defn update-non-nil-keys! {:style/indent 2} ([model id kvs] (update! model id (into {} (for [[k v] kvs :when (not (nil? v))] [k v])))) ([model id k v & more] (update-non-nil-keys! model id (apply array-map k v more)))) | ||||||||||||||||||
INSERT! | |||||||||||||||||||
Different possible keys that might come back for the ID of a newly inserted row. Differs by DB. | (def ^:private inserted-id-keys [;; Postgres, newer H2, and most others return :id :id ;; :generated_key is returned by MySQL :generated_key ;; MariaDB returns :insert_id :insert_id ;; scope_identity() returned by older versions of H2 (keyword "scope_identity()") ;; last_insert_rowid() returned by SQLite3 (keyword "last_insert_rowid()")]) | ||||||||||||||||||
Get the ID of a row inserted by | (defn get-inserted-id [primary-key insert-result] (when insert-result (some insert-result (cons primary-key inserted-id-keys)))) | ||||||||||||||||||
Do a simple JDBC
| (defn simple-insert-many! {:style/indent 1} [model row-maps] {:pre [(sequential? row-maps) (every? map? row-maps)]} (when (seq row-maps) (let [model (resolve-model model) primary-key (models/primary-key model)] (doall (for [row-map row-maps :let [sql (honeysql->sql {:insert-into model, :values [row-map]})]] (->> (jdbc/db-do-prepared-return-keys (connection) false sql {}) ; false = don't use a transaction (get-inserted-id primary-key))))))) | ||||||||||||||||||
Insert several new rows into the Database. Resolves Note: this does not call
| (defn insert-many! {:style/indent 1} [model row-maps] (let [model (resolve-model model)] (simple-insert-many! model (for [row-map row-maps] (models/do-pre-insert model row-map))))) | ||||||||||||||||||
Do a simple JDBC
Like | (defn simple-insert! {:style/indent 1} ([model row-map] {:pre [(map? row-map) (every? keyword? (keys row-map))]} (first (simple-insert-many! model [row-map]))) ([model k v & more] (simple-insert! model (apply array-map k v more)))) | ||||||||||||||||||
Insert a new object into the Database. Resolves For flexibility,
| (defn insert! {:style/indent 1} ([model row-map] {:pre [(map? row-map) (every? keyword? (keys row-map))]} (let [model (resolve-model model)] (when-let [id (simple-insert! model (models/do-pre-insert model row-map))] (models/post-insert (model id))))) ([model k v & more] (insert! model (apply array-map k v more)))) | ||||||||||||||||||
SELECT | |||||||||||||||||||
All of the following functions are based off of the old | |||||||||||||||||||
Select a single object from the database.
| (defn select-one {:style/indent 1} [model & options] (let [fields (model->fields model)] (simple-select-one model (where+ {:select (or fields [:*])} options)))) | ||||||||||||||||||
Select a single
| (defn select-one-field {:style/indent 2} [field model & options] {:pre [(keyword? field)]} (field (apply select-one [model field] options))) | ||||||||||||||||||
Select the
| (defn select-one-id {:style/indent 1} [model & options] (let [model (resolve-model model)] (apply select-one-field (models/primary-key model) model options))) | ||||||||||||||||||
Select the count of objects matching some condition.
| (defn count {:style/indent 1} [model & options] (:count (apply select-one [model [:%count.* :count]] options))) | ||||||||||||||||||
Select objects from the database.
| (defn select {:style/indent 1} [model & options] (simple-select model (where+ {:select (or (model->fields model) [:*])} options))) | ||||||||||||||||||
Select objects from the database, returns a reducible.
| (defn select-reducible {:style/indent 1} [model & options] (simple-select-reducible model (where+ {:select (or (model->fields model) [:*])} options))) | ||||||||||||||||||
Select values of a single field for multiple objects. These are returned as a set if any matching fields
were returned, otherwise
| (defn select-field {:style/indent 2} [field model & options] {:pre [(keyword? field)]} (when-let [results (seq (map field (apply select [model field] options)))] (set results))) | ||||||||||||||||||
Select IDs for multiple objects. These are returned as a set if any matching IDs were returned, otherwise
| (defn select-ids {:style/indent 1} [model & options] (let [model (resolve-model model)] (apply select-field (models/primary-key model) model options))) | ||||||||||||||||||
Select fields
| (defn select-field->field {:style/indent 3} [k v model & options] {:pre [(keyword? k) (keyword? v)]} (into {} (for [result (apply select [model k v] options)] {(k result) (v result)}))) | ||||||||||||||||||
Select FIELD and
| (defn select-field->id {:style/indent 2} [field model & options] (let [model (resolve-model model)] (apply select-field->field field (models/primary-key model) model options))) | ||||||||||||||||||
Select
| (defn select-id->field {:style/indent 2} [field model & options] (let [model (resolve-model model)] (apply select-field->field (models/primary-key model) field model options))) | ||||||||||||||||||
EXISTS? | |||||||||||||||||||
Easy way to see if something exists in the DB.
| (defn exists? {:style/indent 1} ^Boolean [model & kvs] (let [model (resolve-model model)] (boolean (select-one-id model (apply where (h/select {} (models/primary-key model)) kvs))))) | ||||||||||||||||||
DELETE! | |||||||||||||||||||
Delete an object or objects from the application DB matching certain constraints.
Returns
Unlike | (defn simple-delete! {:style/indent 1} ([model] (simple-delete! model {})) ([model conditions] {:pre [(map? conditions) (every? keyword? (keys conditions))]} (let [model (resolve-model model)] (not= [0] (execute! (-> (h/delete-from model) (where conditions)))))) ([model k v & more] (simple-delete! model (apply array-map k v more)))) | ||||||||||||||||||
Delete of object(s). For each matching object, the
| (defn delete! {:style/indent 1} [model & conditions] (let [model (resolve-model model) primary-key (models/primary-key model)] (doseq [object (apply select model conditions)] (models/pre-delete object) (simple-delete! model primary-key (primary-key object))))) | ||||||||||||||||||
Functions for deserializing and hydrating fields in objects fetched from the DB. | (ns toucan.hydrate (:require [toucan [db :as db] [models :as models]])) | ||||||||||||||||||
Counts Destructuring & Restructuring | |||||||||||||||||||
*DISCLAIMER*This I wrote this code at 4 AM nearly 2 years ago and don't remember exactly what it is supposed to accomplish, or why. It generates a sort of path that records the wacky ways in which objects in a collection are nested, and how they fit into sequences; it then returns a flattened sequence of desired objects for easy modification. Afterwards the modified objects can be put in place of the originals by passing in the sequence of modified objects and the path. Nonetheless, it still works (somehow) and is well-tested. But it's definitely overengineered and crying out to be
replaced with a simpler implementation ( Original OverviewAt a high level, these functions let you aggressively flatten a sequence of maps by a key so you can apply some function across it, and then unflatten that sequence.
For your convenience | |||||||||||||||||||
Return a sequence of counts / keywords that can be used to unflatten COLL later.
For each
| (defn- counts-of [coll k] (map (fn [x] (cond (sequential? (k x)) (count (k x)) (k x) :atom (contains? x k) :nil :else nil)) coll)) | ||||||||||||||||||
Flatten COLL by K.
| (defn- counts-flatten [coll k] {:pre [(sequential? coll) (keyword? k)]} (->> coll (map k) (mapcat (fn [x] (if (sequential? x) x [x]))))) | ||||||||||||||||||
Unflatten COLL by K using COUNTS from
| (defn- counts-unflatten ([coll k counts] (counts-unflatten [] coll k counts)) ([acc coll k [count & more]] (let [[unflattend coll] (condp = count nil [nil (rest coll)] :atom [(first coll) (rest coll)] :nil [:nil (rest coll)] (split-at count coll)) acc (conj acc unflattend)] (if-not (seq more) (map (fn [x] (when x {k (when-not (= x :nil) x)})) acc) (recur acc coll k more))))) | ||||||||||||||||||
Apply F to values of COLL flattened by K, then return unflattened/updated results.
| (defn- counts-apply [coll k f] (let [counts (counts-of coll k) new-vals (-> coll (counts-flatten k) f (counts-unflatten k counts))] (map merge coll new-vals))) | ||||||||||||||||||
Util Fns | |||||||||||||||||||
Is this a valid argument to | (defn- valid-hydration-form? [k] (or (keyword? k) (and (sequential? k) (keyword? (first k)) (every? valid-hydration-form? (rest k))))) | ||||||||||||||||||
Append to a keyword.
| (defn- kw-append [k suffix] (keyword (str (name k) suffix))) | ||||||||||||||||||
Return a map of hydration keywords to functions that should be used to hydrate them, e.g.
These functions are ones that are marked METADATA-KEY, e.g. | (defn- lookup-functions-with-metadata-key [metadata-key] (loop [m {}, [[k f] & more] (for [ns (all-ns) [symb varr] (ns-interns ns) :let [hydration-key (metadata-key (meta varr))] :when hydration-key] [(if (true? hydration-key) (keyword (name symb)) hydration-key) varr])] (cond (not k) m (m k) (throw (Exception. (format "Duplicate `^%s` functions for key '%s': %s and %s." metadata-key k (m k) f))) :else (recur (assoc m k f) more)))) | ||||||||||||||||||
Automagic Batched Hydration (via :model-keys) | |||||||||||||||||||
Return map of This is built pulling the | (defn- require-model-namespaces-and-find-hydration-fns [] (into {} (for [ns (all-ns) [_ varr] (ns-publics ns) :let [model (var-get varr)] :when (models/model? model) :let [hydration-keys (models/hydration-keys model)] k hydration-keys] {k model}))) | ||||||||||||||||||
(def ^:private automagic-batched-hydration-key->model* (atom nil)) | |||||||||||||||||||
Get a map of hydration keys to corresponding models. | (defn- automagic-batched-hydration-key->model [] (or @automagic-batched-hydration-key->model* (reset! automagic-batched-hydration-key->model* (require-model-namespaces-and-find-hydration-fns)))) | ||||||||||||||||||
Can we do a batched hydration of RESULTS with key K? | (defn- can-automagically-batched-hydrate? [results k] (let [k-id-u (kw-append k "_id") k-id-d (kw-append k "-id") contains-k-id? (fn [obj] (or (contains? obj k-id-u) (contains? obj k-id-d)))] (and (contains? (automagic-batched-hydration-key->model) k) (every? contains-k-id? results)))) | ||||||||||||||||||
Hydrate keyword DEST-KEY across all RESULTS by aggregating corresponding source keys ( | (defn- automagically-batched-hydrate [results dest-key] {:pre [(keyword? dest-key)]} (let [model ((automagic-batched-hydration-key->model) dest-key) source-keys #{(kw-append dest-key "_id") (kw-append dest-key "-id")} ids (set (for [result results :when (not (get result dest-key)) :let [k (some result source-keys)] :when k] k)) primary-key (models/primary-key model) objs (if (seq ids) (into {} (for [item (db/select model, primary-key [:in ids])] {(primary-key item) item})) (constantly nil))] (for [result results :let [source-id (some result source-keys)]] (if (get result dest-key) result (assoc result dest-key (objs source-id)))))) | ||||||||||||||||||
Function-Based Batched Hydration (fns marked ^:batched-hydrate) | |||||||||||||||||||
(def ^:private hydration-key->batched-f* (atom nil)) | |||||||||||||||||||
Map of keys to functions marked | (defn- hydration-key->batched-f [] (or @hydration-key->batched-f* (reset! hydration-key->batched-f* (lookup-functions-with-metadata-key :batched-hydrate)))) | ||||||||||||||||||
(defn- can-fn-based-batched-hydrate? [_ k] (contains? (hydration-key->batched-f) k)) | |||||||||||||||||||
(defn- fn-based-batched-hydrate [results k] {:pre [(keyword? k)]} (((hydration-key->batched-f) k) results)) | |||||||||||||||||||
Function-Based Simple Hydration (fns marked ^:hydrate) | |||||||||||||||||||
(def ^:private hydration-key->f* (atom nil)) | |||||||||||||||||||
Fetch a map of keys to functions marked | (defn- hydration-key->f [] (or @hydration-key->f* (reset! hydration-key->f* (lookup-functions-with-metadata-key :hydrate)))) | ||||||||||||||||||
Hydrate keyword K in results by calling corresponding functions when applicable. | (defn- simple-hydrate [results k] {:pre [(keyword? k)]} (for [result results] ;; don't try to hydrate if they key is already present. If we find a matching fn, hydrate with it (when result (or (when-not (k result) (when-let [f ((hydration-key->f) k)] (assoc result k (f result)))) result)))) | ||||||||||||||||||
Resetting Hydration keys (for REPL usage) | |||||||||||||||||||
Clear out the cached hydration keys. Useful when doing interactive development and defining new hydration functions. | (defn flush-hydration-key-caches! [] (reset! automagic-batched-hydration-key->model* nil) (reset! hydration-key->batched-f* nil) (reset! hydration-key->f* nil)) | ||||||||||||||||||
Primary Hydration Fns | |||||||||||||||||||
(declare hydrate) | |||||||||||||||||||
Hydrate a nested hydration form (vector) by recursively calling | (defn- hydrate-vector [results [k & more :as vect]] (assert (> (count vect) 1) (format (str "Replace '%s' with '%s'. Vectors are for nested hydration. " "There's no need to use one when you only have a single key.") vect (first vect))) (let [results (hydrate results k)] (if-not (seq more) results (counts-apply results k #(apply hydrate % more))))) | ||||||||||||||||||
Hydrate a single keyword. | (defn- hydrate-kw [results k] (cond (can-automagically-batched-hydrate? results k) (automagically-batched-hydrate results k) (can-fn-based-batched-hydrate? results k) (fn-based-batched-hydrate results k) :else (simple-hydrate results k))) | ||||||||||||||||||
Hydrate a single hydration form. | (defn- hydrate-1 [results k] (if (keyword? k) (hydrate-kw results k) (hydrate-vector results k))) | ||||||||||||||||||
Hydrate many hydration forms across a sequence of RESULTS by recursively calling | (defn- hydrate-many [results k & more] (let [results (hydrate-1 results k)] (if-not (seq more) results (recur results (first more) (rest more))))) | ||||||||||||||||||
Public Interface | |||||||||||||||||||
| |||||||||||||||||||
Hydrate a single object or sequence of objects. Automagic Batched Hydration (via hydration-keys)
Since
The corresponding Function-Based Batched Hydration (via functions marked ^:batched-hydrate) If the key can't be hydrated auto-magically with the appropriate
By default, the function will be used to hydrate keys that match its name; as in the example above, you can specify a different key to hydrate for in the metadata instead. Simple Hydration (via functions marked ^:hydrate) If the key is not eligible for batched hydration,
As with
Keep in mind that you can only define a single function/method to hydrate each key; move functions into the
Hydrating Multiple KeysYou can hydrate several keys at one time:
Nested HydrationYou can do recursive hydration by listing keys inside a vector:
The first key in a vector will be hydrated normally, and any subsequent keys will be hydrated inside the corresponding values for that key.
| (defn hydrate [results k & ks] {:pre [(valid-hydration-form? k) (every? valid-hydration-form? ks)]} (when results (if (sequential? results) (if (empty? results) results (apply hydrate-many results k ks)) (first (apply hydrate-many [results] k ks))))) | ||||||||||||||||||
The | (ns toucan.models (:require [clojure.walk :refer [postwalk]] [honeysql.format :as hformat] [potemkin.types :as p.types] [toucan.util :as u]) (:import honeysql.format.ToSql)) | ||||||||||||||||||
Configuration | |||||||||||||||||||
Root Model Namespace | |||||||||||||||||||
The root model namespace is the parent namespace of all Toucan models. Toucan knows how to automatically load namespaces where models live, which is handy for avoiding circular references; to facilitate this, Toucan models need to live in places that match an expected pattern. For example, a model named The root model namespace defaults to This is almost certainly not what you want; set your own value by calling
After setting the default model root namespace as in the example above, Toucan will look for | |||||||||||||||||||
(defonce ^:private -root-namespace (atom 'models)) | |||||||||||||||||||
Set the root namespace where all models are expected to live.
In this example, Toucan would look for a model named | (defn set-root-namespace! [new-root-namespace] {:pre [(symbol? new-root-namespace)]} (reset! -root-namespace new-root-namespace)) | ||||||||||||||||||
Fetch the parent namespace for all Toucan models. | (defn root-namespace [] @-root-namespace) | ||||||||||||||||||
Types | |||||||||||||||||||
Model types are a easy way to define functions that should be used to transform values of a certain column when they come out from or go into the database. For example, suppose you had a
Whenever you fetch a Venue, Toucan will automatically apply the appropriate
In the other direction,
You can add a new type by calling
In the example above, values of any columns marked as | |||||||||||||||||||
(defonce ^:private type-fns (atom {:keyword {:in u/keyword->qualified-name :out keyword}})) | |||||||||||||||||||
Add a new type mapping for type named by key K. Supply mappings for the functions that should prepare value
when it goes
| (defn add-type! {:style/indent 1} [k & {:keys [in out]}] {:pre [(fn? in) (fn? out)]} (swap! type-fns assoc k {:in in, :out out})) | ||||||||||||||||||
Properties | |||||||||||||||||||
Model properties are a powerful way to extend the functionality of Toucan models. With properties, you can define custom functions that can modify the values (or even add new ones) of an object
before it is saved (via the Properties are global, which lets you define a single set of functions that can be applied to multiple models
that have the same property, without having to define repetitive code in model methods such as For example, suppose you have several models with You could handle this behavior by defining custom implementations for
In this example, before a Venue is inserted, a new value for Property functions can be defined for any combination of
where In the example above, You can set the value to any truthy value you'd like, which can be used to customize behavior for different models, making properties even more flexible. | |||||||||||||||||||
(defonce ^:private property-fns (atom nil)) | |||||||||||||||||||
Define a new model property and set the functions used to implement its functionality. See documentation for more details.
| (defn add-property! {:style/indent 1} [k & {:keys [insert update select]}] {:pre [(or (not insert) (fn? insert)) (or (not update) (fn? update)) (or (not select) (fn? select))]} (swap! property-fns assoc k {:insert insert, :update update, :select select})) | ||||||||||||||||||
IModel Interface | |||||||||||||||||||
The This protocol contains the various methods model classes can optionally implement. All methods have a default
implementation provided by
| (p.types/defprotocol+ IModel (pre-insert [this] "Gets called by `insert!` immediately before inserting a new object. This provides an opportunity to do things like encode JSON or provide default values for certain fields. (pre-insert [query] (let [defaults {:version 1}] (merge defaults query))) ; set some default values") ;TODO add support for composite keys (primary-key ^clojure.lang.Keyword [this] "Defines the primary key. Defaults to :id (primary-key [_] :id) NOTE: composite keys are currently not supported") (post-insert [this] "Gets called by `insert!` with an object that was newly inserted into the database. This provides an opportunity to trigger specific logic that should occur when an object is inserted or modify the object that is returned. The value returned by this method is returned to the caller of `insert!`. The default implementation is `identity`. (post-insert [user] (assoc user :newly-created true)) (post-insert [user] (add-user-to-magic-perm-groups! user) user)") (pre-update [this] "Called by `update!` before DB operations happen. A good place to set updated values for fields like `updated-at`, or to check preconditions.") (post-update [this] "Gets called by `update!` with an object that was successfully updated in the database. This provides an opportunity to trigger specific logic that should occur when an object is updated. The value returned by this method is not returned to the caller of `update!`. The default implementation is `nil` (not invoked). Note: This method is *not* invoked when calling `update!` with a `honeysql-form` form. (post-update [user] (audit-user-updated! user)") (post-select [this] "Called on the results from a call to `select` and similar functions. Default implementation doesn't do anything, but you can provide custom implementations to do things like remove sensitive fields or add dynamic new ones. For example, let's say we want to add a `:name` field to Users that combines their `:first-name` and `:last-name`: (defn- post-select [user] (assoc user :name (str (:first-name user) \" \" (:last-name user)))) Then, when we select a User: (User 1) ; -> {:id 1, :first-name \"Cam\", :last-name \"Saul\", :name \"Cam Saul\"}") (pre-delete [this] "Called by `delete!` for each matching object that is about to be deleted. Implementations can delete any objects related to this object by recursively calling `delete!`, or do any other cleanup needed, or check some preconditions that must be fulfilled before deleting an object. The output of this function is ignored. (pre-delete [{database-id :id :as database}] (delete! Card :database_id database-id) ...)") (default-fields ^clojure.lang.Sequential [this] "Return a sequence of keyword field names that should be fetched by default when calling `select` or invoking the model (e.g., `(Database 1)`).") (hydration-keys ^clojure.lang.Sequential [this] "The `hydration-keys` method can be overrode to specify the keyword field names that should be hydrated as instances of this model. For example, `User` might include `:creator`, which means `hydrate` will look for `:creator_id` or `:creator-id` in other objects to find the User ID, and fetch the `Users` corresponding to those values.") (types ^clojure.lang.IPersistentMap [this] "Return a map of keyword field names to their types for fields that should be serialized/deserialized in a special way. Values belonging to a type are sent through an input function before being inserted into the DB, and sent through an output function on their way out. `:keyword` is the only type enabled by default; you can add more by calling `add-type!`: (add-type! :json, :in json/generate-string, :out json/parse-string) Set the types for a model like so: ;; convert `:category` to a keyword when it comes out of the DB; convert back to a string before going in (types [_] {:category :keyword})") (properties ^clojure.lang.IPersistentMap [this] "Return a map of properties of this model. Properties can be used to implement advanced behavior across many different models; see the documentation for more details. Declare a model's properties as such: (properties [_] {:timestamped? true}) Define functions to handle objects with those properties using `add-property!`: (add-property! :timestamped? :insert (fn [obj] (assoc obj :created-at (new-timestamp), :updated-at (new-timestamp))) :update (fn [obj] (assoc obj :updated-at (new-timestamp))))")) | ||||||||||||||||||
INTERNAL IMPL | |||||||||||||||||||
Apply the appropriate | (defn- apply-type-fns [obj direction] (into obj (for [[col type] (types obj)] (when-let [v (get obj col)] {col ((get-in @type-fns [type direction]) v)})))) | ||||||||||||||||||
(defn- apply-property-fns [context obj] (loop [obj obj, [[k v] & more] (seq (properties obj))] (let [f (get-in @property-fns [k context])] (cond (not k) obj f (recur (f obj v) more) :else (recur obj more))))) | |||||||||||||||||||
Used by internal functions like | (p.types/defprotocol+ ICreateFromMap (^:private map-> [klass, ^clojure.lang.IPersistentMap m] "Convert map M to instance of record type KLASS.")) | ||||||||||||||||||
these functions call (map-> model ...) twice to make sure functions like pre-insert/post-select didn't do something that accidentally removed the typing | |||||||||||||||||||
Don't call this directly! Apply functions like | (defn do-pre-insert [model obj] (as-> obj <> (map-> model <>) (pre-insert <>) (map-> model <>) (apply-type-fns <> :in) (apply-property-fns :insert <>))) | ||||||||||||||||||
Don't call this directly! Apply internal functions like | (defn do-pre-update [model obj] (as-> obj <> (map-> model <>) (pre-update <>) (map-> model <>) (apply-type-fns <> :in) (apply-property-fns :update <>))) | ||||||||||||||||||
Don't call this directly! Apply internal functions like | (defn do-post-select [model obj] (as-> obj <> (map-> model <>) (apply-type-fns <> :out) (post-select <>) (map-> model <>) (apply-property-fns :select <>))) | ||||||||||||||||||
Default implementations for | (def IModelDefaults {:default-fields (constantly nil) :primary-key (constantly :id) :types (constantly nil) :properties (constantly nil) :pre-insert identity :post-insert identity :pre-update identity :post-update nil :post-select identity :pre-delete (constantly nil) :hydration-keys (constantly nil)}) | ||||||||||||||||||
Fetch an object with a specific ID or all objects of type ENTITY from the DB.
| (defn- invoke-model ([model] ((resolve 'toucan.db/select) model)) ([model id] (when id (invoke-model model (primary-key model) id))) ([model k v & more] (apply (resolve 'toucan.db/select-one) model k v more))) | ||||||||||||||||||
Is model a valid toucan model? | (defn model? [model] (and (record? model) (::model model))) | ||||||||||||||||||
Check whether OBJ is an model (e.g. We use the same record type (e.g., | (defn invoke-model-or-instance [obj & args] (apply (if (model? obj) invoke-model get) obj args)) | ||||||||||||||||||
DEFMODEL MACRO | |||||||||||||||||||
Macro helper, generates
| (defn- ifn-invoke-forms [] (let [args (map #(symbol (str "arg" %)) (range 1 19)) arg-lists (reductions conj ['this] args)] (for [l arg-lists] (list 'invoke l (concat `(invoke-model-or-instance) l))))) | ||||||||||||||||||
Make a symbol fully qualified by resolving it as a var in the current namespace.
| (defn- fully-qualified-symbol [s] (let [{:keys [ns name]} (meta (resolve s))] (symbol (str ns "/" name)))) | ||||||||||||||||||
Take in forms as passed to defrecord or extend-type (protocol or interface name followed by method definitions), and return a map suitable for use with extend.
| (defn- method-forms-map [forms] (second (reduce (fn [[type acc] form] (if (symbol? form) [(fully-qualified-symbol form) acc] [type (assoc-in acc [type (keyword (first form))] `(fn ~@(drop 1 form)))])) [nil {}] forms))) | ||||||||||||||||||
Define a new "model". Models encapsulate information and behaviors related to a specific table in the application DB, and have their own unique record type. This class is used for both the titular model (e.g.
The record type automatically extends
This is equivalent to:
Finally, the model itself is invokable. Calling with no args returns all values of that object; calling with a single arg can be used to fetch a specific instance by its integer ID.
| (defmacro defmodel {:arglists '([model table-name] [model docstr? table-name]) :style/indent [2 :form :form [1]]} [model & args] (let [[docstr table-name] (if (string? (first args)) args (list nil (first args))) extend-forms (if (string? (first args)) (drop 2 args) (drop 1 args)) instance (symbol (str model "Instance")) map->instance (symbol (str "map->" instance)) defrecord-form `(p.types/defrecord+ ~instance [] clojure.lang.Named (~'getName [~'_] ~(name model)) (~'getNamespace [~'_] ~(name (ns-name *ns*))) clojure.lang.IFn ~@(ifn-invoke-forms) (~'applyTo [~'this ^clojure.lang.ISeq ~'args] (apply invoke-model-or-instance ~'this ~'args))) ;; Replace the implementation of `empty`. It's either this, or using the ;; lower level `deftype`, and re-implementing all of `defrecord` defrecord-form (postwalk (fn [f] (if (and (seq? f) (= (first f) 'clojure.core/empty)) `(empty [_#] (~map->instance {})) f)) (macroexpand defrecord-form))] `(do ~defrecord-form (extend ~instance ~@(mapcat identity (merge-with (fn [this that] `(merge ~this ~that)) `{toucan.models/IModel toucan.models/IModelDefaults toucan.models/ICreateFromMap {:map-> (fn [~'_ & args#] (apply ~map->instance args#))} honeysql.format/ToSql {:to-sql (comp hformat/to-sql keyword :table)}} (method-forms-map extend-forms)))) (def ~(vary-meta model assoc :tag (symbol (str (namespace-munge *ns*) \. instance)) :arglists ''([] [id] [& kvs]) :doc (or docstr (format "Entity for '%s' table; instance of %s." (name table-name) instance))) (~map->instance {:table ~table-name :name ~(name model) ::model true}))))) | ||||||||||||||||||
Utility functions for writing tests with Toucan models. | (ns toucan.util.test (:require [potemkin.types :as p.types] [toucan.db :as db])) | ||||||||||||||||||
TEMP OBJECTS | |||||||||||||||||||
For your convenience Toucan makes testing easy with Temporary Objects. A temporary object is created and made
available to some body of code, and then wiped from that database via a Here's an example of a unit test using a temporary object created via
In this example, a new instance of Often a Model will require that many fields be
Now whenever we use
You can still override any of the defaults, however:
Finally, Toucan provides a couple more advanced versions of
Each successive object can reference the temp object before it; the form is equivalent to writing multiple
The last helper macro is available if you use the
This macro makes the temporary object available to both the "expected" and "actual" parts of the test. (PRs for similar macros for other unit test frameworks are welcome!) | |||||||||||||||||||
Protocol defining the | (p.types/defprotocol+ WithTempDefaults (with-temp-defaults ^clojure.lang.IPersistentMap [this] "Return a map of default values that should be used when creating a new temporary object of this model. ;; Use a random first and last name for new temporary Users unless otherwise specified (extend-protocol WithTempDefaults (class User) (with-temp-defaults [_] {:first-name (random-name), :last-name (random-name)}))")) | ||||||||||||||||||
default impl | (extend Object WithTempDefaults {:with-temp-defaults (constantly {})}) | ||||||||||||||||||
Internal implementation of | (defn do-with-temp [model attributes f] (let [temp-object (db/insert! model (merge (when (satisfies? WithTempDefaults model) (with-temp-defaults model)) attributes))] (try (f temp-object) (finally (db/delete! model :id (:id temp-object)))))) | ||||||||||||||||||
Create a temporary instance of ENTITY bound to BINDING-FORM, execute BODY,
then deletes it via Our unit tests rely a heavily on the test data and make some assumptions about the DB staying in the same clean state. This allows us to write very concise tests. Generally this means tests should "clean up after themselves" and leave things the way they found them.
| (defmacro with-temp [model [binding-form & [options-map]] & body] `(do-with-temp ~model ~options-map (fn [~binding-form] ~@body))) | ||||||||||||||||||
Like
| (defmacro with-temp* [model-bindings & body] (loop [[pair & more] (reverse (partition 2 model-bindings)), body `(do ~@body)] (let [body `(with-temp ~@pair ~body)] (if (seq more) (recur more body) body)))) | ||||||||||||||||||
EXPECTATIONS HELPER MACROS | |||||||||||||||||||
(defn- has-expectations-dependency? [] (try (require 'expectations) true (catch Throwable _ false))) | |||||||||||||||||||
Combines
| (when (has-expectations-dependency?) (defmacro expect-with-temp {:style/indent 1} [with-temp*-form expected actual] ;; use `gensym` instead of auto gensym here so we can be sure it's a unique symbol every time. Otherwise since ;; expectations hashes its body to generate function names it will treat every usage of `expect-with-temp` as ;; the same test and only a single one will end up being ran (let [with-temp-form (gensym "with-temp-")] `(let [~with-temp-form (delay (with-temp* ~with-temp*-form [~expected ~actual]))] (expectations/expect ;; if dereferencing with-temp-form throws an exception then expect Exception <-> Exception will pass; ;; we don't want that, so make sure the expected is nil (try (first @~with-temp-form) (catch Throwable ~'_)) (second @~with-temp-form)))))) | ||||||||||||||||||
Utility functions used by other Toucan modules. | (ns toucan.util (:require [clojure.string :as s]) (:import java.util.Locale)) | ||||||||||||||||||
Return keyword K as a string, including its namespace, if any (unlike
| (defn keyword->qualified-name [k] (when k (s/replace (str k) #"^:" ""))) | ||||||||||||||||||
Locale-agnostic version of | (defn lower-case [^CharSequence s] (.. s toString (toLowerCase (Locale/US)))) | ||||||||||||||||||