public
Last active

Basic Clojure interaction with SQL

  • Download Gist
database.clj
Clojure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
(ns bookkeeper.database
(:require [clojure.java.jdbc :as sql]
[clj-time.format :as f]))
 
(def db {:classname "com.mysql.jdbc.Driver"
:subprotocol "mysql"
:subname "//database-server:3306/database"
:user "user"
:password "password"})
 
;; ----------------------------------------------------------------------------
;; Explicit connections prevent composition. So we wrap all db functions in
;; a fn that checks if a connection exists, and only if not does it make one
;; that way the outermost fn that needs a db creates the connection and those
;; internal use it.
 
(defmacro wrap-connection [& body]
`(if (sql/find-connection)
~@body
(sql/with-connection db ~@body)))
 
(defmacro transaction [& body]
`(if (sql/find-connection)
(sql/transaction ~@body)
(sql/with-connection db (sql/transaction ~@body))))
 
;; ---------------------------------------------------------------------------
;; Utilities
(def mysql-formatter
(f/formatter "YYYYMMddHHmmss"))
 
(defn fmt-time [t]
(f/unparse mysql-formatter t))
 
;; ---------------------------------------------------------------------------
;; A protocal that must be followed by anything DB persistable
(defprotocol DB-ENTRY
(insert [entry])
(delete [entry]))
 
;; ----------------------------------------------------------------------------
(defn- convert-time
"If the map contains a :time key then convert it to SQL time"
[m]
(if (contains? m :time)
(assoc m :time (fmt-time (:time m)))
m))
 
(defn- format-for-ins
"Preformat a map for insertion into the database. Removes id field and
converts time"
[m]
(-> m
(dissoc :id)
(convert-time)))
 
;; ----------------------------------------------------------------------------
(defn ins
"Inserts a map into the database. The keys of the map must map onto the fields
of the database. In most cases we have an
autoincrement field. So fetch and return the map with this included."
[in-m table]
(wrap-connection
(let [m (format-for-ins in-m)
ins (sql/insert-values table (keys m) (vals m))]
;; Bind in the returned primary key
(assoc in-m :id (:generated_key ins)))))
 
;; ----------------------------------------------------------------------------
(defn del
"Remove a map from the database via the passed in primary key"
[m table]
(wrap-connection
(sql/delete-rows
table
["id=?" (:id m)])))
 
;; ----------------------------------------------------------------------------
;; Playing to see where this should go
(defn select-first [query]
"Return the first result of query in a vector."
(wrap-connection
(sql/with-query-results res
query
(into {} (first res)))))
 
;; ----------------------------------------------------------------------------
(defn select-all [query]
"Return all results of query in a vector. No ordering guarantees are made."
(wrap-connection
(sql/with-query-results res
query
(vec (map (partial into {}) res)))))

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.