Introduction
clojure.jdbc is a library for low level, jdbc-based database access.
Install
The simplest way to use clojure.jdbc in a clojure project, is by including it in the dependency vector on your project.clj file:
[funcool/clojure.jdbc "0.9.0"]
You also need to add an entry for the java driver that you need. For instance, for mysql
:
---
[mysql/mysql-connector-java "5.1.6"]
---
The project is tested under JDK7 and JDK8.
Project Maturity
Since clojure.jdbc is a young project there may be some API breakage.
User Guide
Connecting to database
This section intends to explain everything that you should to know about how connect to the database.
Connection parameters
JDBC is the default Java abstraction/interface for SQL databases. It’s like the Python DB-API and similar abstractions in other languages. Clojure, as a guest language on the JVM, benefits from having a good, well-tested abstraction like that.
Connection parameters are exposed in a simple hash-map and called dbspec. This is the simplest and most idiomatic way in Clojure to define configuration parameters.
(def dbspec {:subprotocol "postgresql"
:subname "//localhost:5432/dbname"
:user "username" ;; Optional
:password "password"} ;; Optional
Also, clojure.jdbc comes with alternative, more human comprehensible format, like this:
(def dbspec {:vendor "postgresql"
:name "dbname"
:host "localhost" ;; Optional
:port 5432 ;; Optional
:user "username" ;; Optional
:password "password"}) ;; Optional
But it has some disadvantages: it does not supports all options of a default dbspec format.
Also, dbspec can be represented as a URI.
(def dbspec "postgresql://user:password@localhost:5432/dbname")
Creating a connection
With clojure.jdbc every function that interacts with a database explicitly requires one connection instance as parameter (no dynamic vars are used for it).
connection
function:(require '[jdbc.core :as jdbc])
(let [conn (jdbc/connection dbspec)]
(do-something-with conn)
(.close conn))
As you can see in previous example, you should explicltly close connection for proper
resource management. You can use the with-open
clojure macro for make code looks
more clean and idiomatic.
(with-open [conn (jdbc/connection dbspec)]
(do-something-with conn))
Execute Database Commands
Execute Raw SQL Statements
The simplest way to execute raw SQL is using the execute
function. It requires
an active connection as the first parameter followed by SQL sentence:
(with-open [conn (jdbc/connection dbspec)]
(jdbc/execute conn "CREATE TABLE foo (id serial, name text);"))
Execute Parametrized SQL Statements
Raw SQL statements work well for creating tables and similar operations, but when you need to insert some data, especially if the data comes from untrusted sources. Using plain string concatenation is a very bat and unsafe approach.
For safe parameter bindings, it there a sqlvec format that consists in a vector with sql followed of parameters:
["select * from foo where age < ?" 20]
Almost all functions in clojure.jdbc accepts sqlvec as query parameter.
Let see an example using it in execute
function:
(jdbc/execute conn ["insert into foo (name) values (?);" "Foo"]))
Returning Inserted Keys
In some circumstances, you want to use the "RETURNING id" or similar functionality on your queries for getting the primary keys of newly inserted records.
(let [sql "insert into foo (name) values (?) returning id;"
res (jdbc/fetch conn [sql "Foo"])]
(println res))
;; This should print something like this to standard output:
[{:id 3}]
Make Queries
It is time to start executing queries and fetch results. For this purpose,
clojure.jdbc exposes the fetch
function:
(let [sql ["SELECT id, name FROM people WHERE age > ?", 2]
result (jdbc/fetch conn sql)]
(doseq [row result]
(println row))))
;; It should print somthing like this:
;; => {:id 1 :name "Foo"}
;; => {:id 2 :name "Bar"}
We are know that almost all function in clojure.jdbc accepts a plain
sql and sqlvec as query parameter. But is not entirely true, it also
accepts as query parameter an instance of self crafted PreparedStatement
or anythig that implements the ISQLStatement
protocol.
(let [stmt (jdbc/prepared-statement ["SELECT id, name FROM people WHERE age > ?", 2])
result (jdbc/fetch conn stmt)]
(println "Result: " result))
Note
|
The |
Transactions
Getting Started with Transactions
The most idiomatic way to wrap some code in a transaction, is by using the atomic
macro:
(jdbc/atomic conn
(do-thing-first conn)
(do-thing-second conn))
Note
|
clojure.jdbc does not uses any dynamic thread-local vars to store the transaction state
of a connection. Instead of that, it overwrites the lexical scope value of |
Low-level Transaction Primitives
Behind the scenes of the atomic
macro, clojure.jdbc uses the atomic-apply
function.
Given an active connection as the first parameter and function that you want execute in a transaction as the second parameter, it executes the function inside a database transaction. The callback function should accept a connection as its first parameter.
(jdbc/atomic-apply conn
(fn [conn]
(do-something-with conn)))
Note
|
clojure.jdbc, in contrast to java.jdbc, handles nested transactions well. Thus making all code wrapped in transaction blocks truly atomic independently of transaction nesting. If you want extend or change a default transaction strategy, see Transaction Strategy section. |
Isolation Level
clojure.jdbc by default does nothing with the isolation level and keeps it to default values.
(def dbspec {:subprotocol "h2"
:subname "mem:"
:isolation-level :serializable})
(with-open [conn (jdbc/connection dbspec)]
;; The just created connection has the isolation
;; level set to :serializable
(do-things conn))
An other way to set the isolation level is in moment of declaring a transaction, using
the atomic-apply
function or atomic
macro:
(jdbc/atomic-apply conn do-something {:isolation-level :serializable})
(jdbc/atomic conn {:isolation-level :serializable}
(do-something conn))
This is a list of supported options:
-
:read-uncommitted
- Set read uncommitted isolation level -
:read-committed
- Set read committed isolation level -
:repeatable-read
- Set repeatable reads isolation level -
:serializable
- Set serializable isolation level -
:none
- Use this option to indicate to clojure.jdbc to do nothing and keep default behavior.
You can read more about it on wikipedia.
Warning
|
not all JDBC providers support the above isolation levels. |
Read-Only Transactions
In some circumstances, mainly when you are using the strictest isolation-level, you may want to indicate to database that a query is actually read-only, allowing the database server to make some optimizations for this operation.
(jdbc/atomic conn {:isolation-level :serializable
:read-only true}
(query-something conn))
Advanced usage
Server Side Cursors
By default, most JDBC drivers prefetch all results into memory make the use of lazy structures totally useless for fetching data. Luckily, some databases implement server-side cursors that avoid this behavior.
If you have an extremely large resultset and you want retrieve it and process each item, this is exactly what you need.
For this purpose, clojure.jdbc exposes the fetch-lazy
function, that returns a some kind of
cursor instance. At the moment of creating cursor, no query is executed.
The cursor can be used converting it into clojure lazyseq using cursor→lazyseq
function:
(jdbc/atomic conn
(with-open [cursor (jdbc/fetch-lazy conn "SELECT id, name FROM people;")]
(doseq [row (jdbc/cursor->lazyseq cursor)]
(println row)))
In some databases, it requires that cursor should be evaluated in a context of one transaction.
Transaction Strategy
Transaction strategies in clojure.jdbc are implemented using protocols having default implementation explained in the previous sections. This approach allows an easy way to extend, customize or completely change a transaction strategy for your application.
If you want another strategy, you should create a new type and implement the
ITransactionStrategy
protocol.
(require '[jdbc.proto :as proto])
(def dummy-tx-strategy
(reify
proto/ITransactionStrategy
(begin! [_ conn opts] conn)
(rollback! [_ conn opts] conn)
(commit! [_ conn opts] conn)))
clojure.jdbc has different ways to specify that transaction strategy shouldbe used. The most common is setting it in your dbspec:
(def dbspec {:subprotocol "postgresql"
:subname "//localhost:5432/dbname"
:tx-strategy dummy-tx-strategy})
(with-open [conn (jdbc/connection dbspec)]
(jdbc/atomic conn
;; In this transaction block, the dummy transaction
;; strategy will be used.
(do-somthing conn)))
Internally, clojure.jdbc maintains an instance of default transaction strategy stored in a dynamic var. You can use the clojure facilities for alter that var for set an other default transaction stragegy:
alter-var-root
(alter-var-root #'jdbc/*default-tx-strategy* (fn [_] dummy-tx-strategy))
(binding [jdbc/*default-tx-strategy* dummy-tx-strategy]
(some-func-that-uses-transactions))
Extend SQL Types
If you want to extend some type/class to use it as JDBC parameter without explicit conversion
to an SQL-compatible type, you should extend your type with the jdbc.proto/ISQLType
protocol.
Here is an example which extends Java’s String[] (string array) in order to pass it as a query parameter that corresponds to PostgreSQL text array in the database:
(require '[jdbc.proto :as proto])
(extend-protocol ISQLType
;; Obtain a class for string array
(class (into-array String []))
(set-stmt-parameter! [this conn stmt index]
(let [value (proto/as-sql-type this conn)
array (.createArrayOf conn "text" value)]
(.setArray stmt index array)))
(as-sql-type [this conn] this))
In this way you can pass a string array as a JDBC parameter that is automatically converted to an SQL array and assigned properly in a prepared statement:
(with-open [conn (jdbc/connection pg-dbspec)]
(jdbc/execute conn "CREATE TABLE arrayfoo (id integer, data text[]);")
(let [mystringarray (into-array String ["foo" "bar"])]
(jdbc/execute conn ["INSERT INTO arrayfoo VALUES (?, ?);" 1 mystringarray])))
clojure.jdbc also exposes the jdbc.proto/ISQLResultSetReadColumn
protocol that encapsulates
reverse conversions from SQL types to user-defined types.
You can read more about that in this blog post: http://www.niwi.be/2014/04/13/postgresql-json-field-with-clojure-and-jdbc/
Connection pool
DataSource is the preferd way to connect to the database in production enviroments, and is usually used for implement connection pools.
To make good use of resourses is much recommendable use some kind of connection pool implementation. This can avoid continuosly creating and destroying connections, that in the majority of time is a slow operation.
Java ecosystem comes with various of it. This is a list of most used:
-
Apache DBCP2: http://commons.apache.org/proper/commons-dbcp/
clojure.jdbc is compatible with any other connection pool implemenetation, simply
pass a javax.sql.DataSource
instance to jdbc/connection
function.
c3p0
c3p0, a mature, highly concurrent JDBC connection pooling library for clojure.jdbc.
[com.mchange/c3p0 "0.9.5"]
In order to use this connection pool, previously you should create a DataSource instance. Here an little example on how it can be done:
(import 'com.mchange.v2.c3p0.ComboPooledDataSource)
(def ds (doto (ComboPooledDataSource.)
(.setJdbcUrl (str "jdbc:"
(:subprotocol dbspec)
(:subname dbspec)))
(.setUser (:user dbspec nil))
(.setPassword (:password dbspec nil))
;; Pool Size Management
(.setMinPoolSize 3)
(.setMaxPoolSize 15)
;; Connection eviction
(.setMaxConnectionAge 3600) ; 1 hour
(.setMaxIdleTime 1800) ; 1/2 hour
(.setMaxIdleTimeExcessConnections 120)
;; Connection testing
(.setTestConnectionOnCheckin false)
(.setTestConnectionOnCheckout false)
(.setIdleConnectionTestPeriod 600)))
You can found all configuration parameters here: http://www.mchange.com/projects/c3p0/#configuration
Now, the new created datasource should be used like a plain dbspec for creating connections:
(with-open [conn (jdbc/connection ds)]
(do-stuff conn))
dbcp2
Apache commons DBCP (JDBC) connection pool implementation for clojure.jdbc
[org.apache.commons/commons-dbcp2 "2.0.1"]
In order to use this connection pool, previously you should create a DataSource instance. Here an little example on how it can be done:
(import 'org.apache.commons.dbcp2.BasicDataSource)
(def ds (doto (BasicDataSource.)
(.setJdbcUrl (str "jdbc:"
(:subprotocol dbspec)
(:subname dbspec)))
(.setUser (:user dbspec nil))
(.setPassword (:password dbspec nil))
;; Pool Size Management
(.setInitialSize 0)
(.setMaxIdle 3)
(.setMaxTotal 15)
;; Connection eviction
(.setMaxConnLifetimeMillis 3600000) ; 1 hour
;; Connection testing
(.setTestOnBorrow false)
(.setTestOnReturn false)
(.setTestWhileIdle true)
(.setTimeBetweenEvictionRunsMillis 600000) ;; 10 minutes
(.setNumTestsPerEvictionRun 4)
(.setMinEvictableIdleTimeMillis 1800000))) ;; 1/2 hours
You can found all configuration parameters here: http://commons.apache.org/proper/commons-dbcp/configuration.html
Now, the new created datasource should be used like a plain dbspec for creating connections:
(with-open [conn (jdbc/connection ds)]
(do-stuff conn))
HikariCP
Fast, simple, reliable. HikariCP is a "zero-overhead" production ready JDBC connection pool.
[hikari-cp "1.2.4"]
[hikari-cp "1.2.4" :exclusions [com.zaxxer/HikariCP]]
[com.zaxxer/HikariCP-java6 "2.3.9"]
In order to use this connection pool, previously you should create a DataSource instance. Here an little example on how it can be done:
(require '[hikari-cp.core :as hikari])
(def ds (hikari/make-datasource
{:connection-timeout 30000
:idle-timeout 600000
:max-lifetime 1800000
:minimum-idle 10
:maximum-pool-size 10
:adapter "postgresql"
:username "username"
:password "password"
:database-name "database"
:server-name "localhost"
:port-number 5432}))
HikariCP, unlike other datasource implementations, requires to setup explicitly that adapter should be used. This is a list of supported adapters:
Adapter | Datasource class name |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
You can found more information and documentation about hikari-cp here: https://github.com/tomekw/hikari-cp
Now, the new created datasource should be used like a plain dbspec for creating connections:
(with-open [conn (jdbc/connection ds)]
(do-stuff conn))
FAQ
Why another JDBC wrapper?
This is an incomplete list of reasons:
-
Connection management in clojure.jdbc is simple and explicit. However, java.jdbc does not make diferentiation between a connection or dbspec hash map. At first glace it seems more flexible, but it requires huge additional complexity and boilerplate code on each function that receives a connection. If you are curious, take a look at the
with-db-connection
implementation of java.jdbc and compare it with clojure.jdbc: it will get you a good idea of the hidden and unnecessary complexity found in java.jdbc. -
clojure.jdbc comes with proper transaction management with full support for nested transactions, and plugable transaction strategies. In contrast, java.jdbc comes with one unique strategy, that does not support subtransactions (it flattens all nested transactions in one unique giving you false security when you wraps you code in a transaction).
-
clojure.jdbc has native support for connection pools.
-
clojure.jdbc has first class documentation.
-
clojure.jdbc has a simpler implementation than java.jdbc without unnecesary code duplication. A good example are the crud methods of java.jdbc: all them repeats the transaction logic implicitly. In contrast, clojure.jdbc, if you want a transaction, you should wrap your code in a transaction context explicitly.
Does clojure.jdbc have better performance than java.jdbc?
Mostly Yes, clojure.jdbc by default has better performance than java.jdbc. You can
run the micro benchmark code in your environment with: lein with-profile bench run
In my environments, the results are:
[3/5.0.5]niwi@niwi:~/clojure.jdbc> lein with-profile bench run
Simple query without connection overhead.
java.jdbc:
"Elapsed time: 673.890131 msecs"
clojure.jdbc:
"Elapsed time: 450.329706 msecs"
Simple query with connection overhead.
java.jdbc:
"Elapsed time: 2490.233925 msecs"
clojure.jdbc:
"Elapsed time: 2239.524395 msecs"
Simple query with transaction.
java.jdbc:
"Elapsed time: 532.151667 msecs"
clojure.jdbc:
"Elapsed time: 602.482932 msecs"
Why does clojure.jdbc not include a DSL for working with SQL as java.jdbc 0.3 does?
clojure.jdbc is a wrapper for the Java JDBC interface. It doesn’t intend to provide helpers to avoid SQL usage. There are already plenty of DSLs for working with SQL. clojure.jdbc will not reinvent the wheel.
This is an incomplete list of Clojure DSLs for SQL:
Is this a fork of java.jdbc?
No. It is an alternative implementation.
Developers Guide
Philosophy
Five most important rules:
-
Beautiful is better than ugly.
-
Explicit is better than implicit.
-
Simple is better than complex.
-
Complex is better than complicated.
-
Readability counts.
All contributions to clojure.jdbc should keep these important rules in mind.
Contributing
Unlike Clojure and other Clojure contributed libraries clojure.jdbc does not have many restrictions for contributions. Just open an issue or pull request.
Source Code
clojure.jdbc is open source and can be found on github.
You can clone the public repository with this command:
git clone https://github.com/funcool/clojure.jdbc
Run tests
For running tests just execute this:
lein test
You should have postgresql up and running with a current user created with trust access mode activated for this user and test db already created.
License
clojure.jdbc is writen from scratch and is licensed under Apache 2.0 license:
Copyright (c) 2013-2015 Andrey Antukh <niwi@niwi.nz> Licensed under the Apache License, Version 2.0 (the "License") you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
You can see the full license in the LICENSE file located in the root of the project repo.
Additionaly, I want to give thanks to the java.jdbc
developers for their good
initial work. Some intial ideas for clojure.jdbc are taken from that project.