aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorscgilardi <scgilardi@gmail.com>2008-10-06 02:31:05 +0000
committerscgilardi <scgilardi@gmail.com>2008-10-06 02:31:05 +0000
commitaaa467fd2eb6b2ca60b4bd7f2f1d6e0f7756a59c (patch)
tree9f7594dd4dab1b597318aaa61fe418b4404417d7 /src
parent2d5f5de88c91b56cd9b7103a6750091774801ce1 (diff)
sql: updates to be more modular, explicit transactions that nest, db-spec as data rather than as a function, with-results changed to bind a seq of results rather than records repeatedly, see clojure.contrib.sql.test for new examples.
Diffstat (limited to 'src')
-rw-r--r--src/clojure/contrib/sql/sql.clj138
-rw-r--r--src/clojure/contrib/sql/sql_internal.clj (renamed from src/clojure/contrib/sql/internal.clj)4
-rw-r--r--src/clojure/contrib/sql/test/test.clj93
3 files changed, 142 insertions, 93 deletions
diff --git a/src/clojure/contrib/sql/sql.clj b/src/clojure/contrib/sql/sql.clj
index 08c162ec..7f257e08 100644
--- a/src/clojure/contrib/sql/sql.clj
+++ b/src/clojure/contrib/sql/sql.clj
@@ -16,48 +16,71 @@
;; Created 2 April 2008
(ns clojure.contrib.sql
- (:import
- (java.sql DriverManager Connection PreparedStatement ResultSet)
- (java.util Properties))
- (:load "internal.clj"))
+ (:use clojure.contrib.except)
+ (:load "sql_internal.clj"))
-(defn connection
- "Returns a connection to a database via a jdbc URL. Additional options
- for the connection may be specified either as a map from keywords to
- values or as inline keywords and values after db-name"
- [subprotocol db-name & opts]
- (DriverManager/getConnection
- (format "jdbc:%s:%s" subprotocol db-name)
- (properties (if (keyword? (first opts))
- (apply hash-map opts)
- (first opts)))))
+(def *db* {:con nil :level 0})
(defmacro with-connection
- "Evaluates body in the context of a connection to a database. Any updates
- are committed as one transaction after evaluating body or rolled back on
- any uncaught exception."
- [con init & body]
- `(with-open ~con ~init
- (try
- (.setAutoCommit ~con false)
- ~@body
- (.commit ~con)
- (catch Exception e#
- (.rollback ~con)
- (throw (Exception. "transaction rolled back" e#))))))
+ "Evaluates body in the context of a new connection to a database then
+ closes it. db-spec is a map containing string values for these required
+ keys:
+ :classname the jdbc driver class name
+ :subprotocol the jdbc subprotocol
+ :subname the jdbc subname
+ db-spec may contain additional key-value pairs that are passed along to
+ the driver as properties such as :user, :password, etc."
+ [db-spec & body]
+ `(do
+ (clojure.lang.RT/classForName (:classname ~db-spec))
+ (with-open con#
+ (java.sql.DriverManager/getConnection
+ (format "jdbc:%s:%s" (:subprotocol ~db-spec) (:subname ~db-spec))
+ (properties (dissoc ~db-spec :classname :subprotocol :subname)))
+ (binding [*db* (assoc *db* :con con# :level 0)]
+ ~@body))))
+
+(defmacro transaction
+ "Evaluates body as a transaction on the open database connection. Updates
+ are committed together as a group after evaluating body or rolled back on
+ any uncaught exception. Any nested transactions will be absorbed into the
+ outermost transaction."
+ [& body]
+ `(let [con# (:con *db*)
+ level# (:level *db*)]
+ (throw-if (not con#) "no database connection")
+ (binding [*db* (assoc *db* :level (inc level#))]
+ (let [auto-commit# (.getAutoCommit con#)]
+ (when (zero? level#)
+ (.setAutoCommit con# false))
+ (try
+ (let [value# (do ~@body)]
+ (when (zero? level#)
+ (.commit con#))
+ value#)
+ (catch Exception e#
+ (.rollback con#)
+ (throw (Exception.
+ (format "transaction rolled back: %s"
+ (.getMessage e#)) e#)))
+ (finally
+ (when (zero? level#)
+ (.setAutoCommit con# auto-commit#))))))))
(defn do-commands
- "Executes SQL commands that do not return results"
- [con & commands]
- (with-open stmt (.createStatement con)
+ "Executes SQL commands that don't return results on the open database
+ connection"
+ [& commands]
+ (with-open stmt (.createStatement (:con *db*))
(doseq cmd commands
(.addBatch stmt cmd))
(.executeBatch stmt)))
(defn do-prepared
- "Executes a prepared statement with parameter sets"
- [con sql & sets]
- (with-open stmt (.prepareStatement con sql)
+ "Executes a prepared statement on the open database connection with
+ parameter sets"
+ [sql & sets]
+ (with-open stmt (.prepareStatement (:con *db*) sql)
(doseq set sets
(doseq [index value] (map vector (iterate inc 1) set)
(.setObject stmt index value))
@@ -65,51 +88,52 @@
(.executeBatch stmt)))
(defn create-table
- "Creates a table given a name (a string or keyword) and column specs. A
- column spec is a vector containing a name, a type, and optionally other
- items such as constraints, each a string or keyword."
- [con name & cols]
- (do-commands con
- (format "create table %s (%s)"
- (the-str name)
- (apply str
+ "Creates a table on the open database connection given a name (a string
+ or keyword) and column specs. A column spec is a vector containing a name
+ and optionally a type and other items such as constraints, each a string
+ or keyword."
+ [name & cols]
+ (do-commands
+ (format "create table %s (%s)"
+ (the-str name)
+ (apply str
(map the-str
(apply concat
(interpose [", "]
(map (partial interpose " ") cols))))))))
(defn drop-table
- "Drops a table give its name (a string or keyword)"
- [con name]
- (do-commands con
- (format "drop table %s" (the-str name))))
+ "Drops a table on the open database connection given its name (a string
+ or keyword)"
+ [name]
+ (do-commands
+ (format "drop table %s" (the-str name))))
(defn insert-values
- "Inserts values into columns of a table. Columns is a seq of column
- names (strings or keywords) and each value is a seq of values for those
- columns. To insert complete rows (all columns), use insert-rows."
- [con table columns & values]
+ "Inserts values into columns of a table. columns is a vector of column
+ names (strings or keywords) and each value is a vector of values for
+ those columns. To insert complete rows (all columns), use insert-rows."
+ [table columns & values]
(let [count (count (first values))
template (apply str (interpose "," (replicate count "?")))
cols (if (seq columns)
(format "(%s)" (apply str (interpose "," (map the-str columns))))
"")]
(apply do-prepared
- con
(format "insert into %s %s values (%s)" (the-str table) cols template)
values)))
(defn insert-rows
- "Inserts complete rows into a table. Each row is a seq of values for
+ "Inserts complete rows into a table. Each row is a vector of values for
each of the table's columns in order."
- [con table & rows]
- (apply insert-values con table nil rows))
+ [table & rows]
+ (apply insert-values table nil rows))
(defmacro with-results
- "Executes a query and then evaluates body repeatedly with rec bound to
- each of the generated results in turn"
- [rec con sql & body]
- `(with-open stmt# (.prepareStatement ~con ~sql)
+ "Executes a query and then evaluates body with res bound to a seq of the
+ results"
+ [res sql & body]
+ `(with-open stmt# (.prepareStatement (:con *db*) ~sql)
(with-open rset# (.executeQuery stmt#)
- (doseq ~rec (resultset-seq rset#)
+ (let [~res (resultset-seq rset#)]
~@body))))
diff --git a/src/clojure/contrib/sql/internal.clj b/src/clojure/contrib/sql/sql_internal.clj
index 0880d491..f58ad4be 100644
--- a/src/clojure/contrib/sql/internal.clj
+++ b/src/clojure/contrib/sql/sql_internal.clj
@@ -11,12 +11,12 @@
;; scgilardi (gmail)
;; Created 3 October 2008
-(defn- properties
+(defn properties
"Converts a Clojure map from keywords or symbols to values into a
java.util.Properties object that maps the names of the keywords or
symbols to the String representation of the values"
[m]
- (let [p (Properties.)]
+ (let [p (java.util.Properties.)]
(when m
(loop [[key & keys] (keys m)
[val & vals] (vals m)]
diff --git a/src/clojure/contrib/sql/test/test.clj b/src/clojure/contrib/sql/test/test.clj
index 94a5a644..eb212dc5 100644
--- a/src/clojure/contrib/sql/test/test.clj
+++ b/src/clojure/contrib/sql/test/test.clj
@@ -14,47 +14,72 @@
;; Created 13 September 2008
(ns clojure.contrib.sql.test
- (:use clojure.contrib.sql))
+ (:require [clojure.contrib.sql :as sql]))
-(clojure.lang.RT/classForName "org.apache.derby.jdbc.EmbeddedDriver")
+(def db {:classname "org.apache.derby.jdbc.EmbeddedDriver"
+ :subprotocol "derby"
+ :subname "/tmp/clojure.contrib.sql.test.db"
+ :create true})
-(defn db []
- (connection "derby" "/tmp/clojure.contrib.sql.test.db" :create true))
+(defn drop-fruit []
+ (try
+ (sql/drop-table :fruit)
+ (catch Exception e)))
+
+(defn create-fruit []
+ (sql/create-table :fruit
+ [:name "varchar(32)" "NOT NULL"]
+ [:appearance "varchar(32)"]
+ [:cost :int]
+ [:grade :real]))
+
+(defn insert-rows-fruit []
+ (sql/insert-rows :fruit
+ ["Apple" "red" 59 87]
+ ["Banana" "yellow" 29 92.2]
+ ["Peach" "fuzzy" 139 90.0]
+ ["Orange" "juicy" 89 88.6]))
+
+(defn insert-values-fruit []
+ (sql/insert-values :fruit
+ [:name :cost]
+ ["Mango" 722]
+ ["Feijoa" 441]))
(defn db-write []
- (with-connection con (db)
- (try
- (drop-table con :fruit)
- (catch Exception e))
- (create-table con :fruit
- [:name "varchar(32)" "NOT NULL"]
- [:appearance "varchar(32)"]
- [:cost :int]
- [:grade :real])
- (insert-rows con :fruit
- ["Apple" "red" 59 87]
- ["Banana" "yellow" 29 92.2]
- ["Peach" "fuzzy" 139 90.0]
- ["Orange" "juicy" 89 88.6])
- (insert-values con :fruit [:name :cost]
- ["Mango" 722]
- ["Feijoa" 441])))
+ (sql/with-connection db
+ (sql/transaction
+ (drop-fruit)
+ (create-fruit)
+ (insert-rows-fruit)
+ (insert-values-fruit)))
+ nil)
(defn db-read []
- (with-connection con (db)
- (with-results rec con
- "select * from fruit"
- (println rec))))
+ (sql/with-connection db
+ (sql/with-results res
+ "select * from fruit"
+ (doseq rec res
+ (println rec)))))
+
+(defn db-read-all []
+ (sql/with-connection db
+ (sql/with-results res
+ "select * from fruit"
+ (into [] res))))
(defn db-grade-a []
- (with-connection con (db)
- (with-results rec con
- "select name, cost from fruit where grade >= 90"
- (println rec))))
+ (sql/with-connection db
+ (sql/with-results res
+ "select name, cost from fruit where grade >= 90"
+ (doseq rec res
+ (println rec)))))
(defn db-exception []
- (with-connection con (db)
- (insert-values con :fruit [:name :appearance]
- ["Grape" "yummy"]
- ["Pear" "bruised"])
- (throw (Exception. "an exception"))))
+ (sql/with-connection db
+ (sql/transaction
+ (sql/insert-values :fruit
+ [:name :appearance]
+ ["Grape" "yummy"]
+ ["Pear" "bruised"])
+ (throw (Exception. "an exception")))))