Clojure: using java.time with clojure.java.jdbc


Java 8 introduced java.time to replace the existing java representations of time java.util.Date, java.sql.Timestamp etc. There were many problems with this old implementation: it wasn't thread safe, it had a clunky API and no built in representation for timezones. java.time is the successor to the fantastic joda.time project which solves all these problems. So if java.time fixes everything then why this article? Well, java.sql.Timestamp still rears its ugly head at the database layer where it is still the default representation of time in the java.jdbc database drivers. In this article we will cover how to automate the conversion between java.sql.Timestamp and java.time so you never have to deal with java.sql.Timestamp again.

Initial set up

Start postgres.

pg_ctl -D pg start

Create a database called databasename.

createdb databasename

Dependencies

Add org.clojure/java.jdbc and org.postgresql/postgresql to your dependencies.

(defproject jdbc-java-time-example "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [org.clojure/java.jdbc "0.7.7"]
                 [org.postgresql/postgresql "42.2.3"]])

Database connection

Start with importing all the needed dependencies, creating a database connection and a test table.

(ns jdbc-java-time-example.core
  (:require [clojure.java.jdbc :as jdbc])
  (:import [java.sql Timestamp]
           [java.sql Date]
           [java.time.format DateTimeFormatter]
           [java.time LocalDate]
           [java.time Instant]
           [java.io FileWriter]))

(def database-connection "postgresql://localhost:5432/databasename")

(jdbc/execute!
   database-connection
   "CREATE TABLE event (pid SERIAL PRIMARY KEY, name text,
   created timestamp with time zone,
   log_date date )")

Extending JDBC with java.time

Extend jdbc/IResultsetReadColumn to convert java.sql.Timestamp to java.time.Instant and java.sql.Date to java.time.LocalDate.

(extend-protocol jdbc/IResultSetReadColumn
  java.sql.Timestamp
  (result-set-read-column [v _ _]
    (.toInstant v))
  java.sql.Date
  (result-set-read-column [v _ _]
    (.toLocalDate v)))

Extend jdbc/ISQLValue to convert java.time.Instant to java.sql.Timestamp and java.time.LocalDate to java.sql.Date.

(extend-protocol jdbc/ISQLValue
  java.time.Instant
  (sql-value [v]
    (Timestamp/from v))
  java.time.LocalDate
  (sql-value [v]
    (Date/valueOf v)))

Read and write time

Inserting java.time values works as expected.

(jdbc/insert!
   database-connection
   :event
   {:name     "page_viewed"
    :created  (Instant/now)
    :log_date (LocalDate/now)})

=> ({:pid 1,
     :name "page_viewed",
     :created #object[java.time.Instant 0x1d43ad18 "2019-08-03T16:28:25.935Z"],
     :log_date #object[java.time.LocalDate 0x265e4f6d "2019-08-03"]})

Same for reading java.time values.

(jdbc/query
   database-connection
   ["select * from event"])

=> ({:pid 1,
     :name "page_viewed",
     :created #object[java.time.Instant 0x1d43ad18 "2019-08-03T16:28:25.935Z"],
     :log_date #object[java.time.LocalDate 0x265e4f6d "2019-08-03"]})

Reader literals

To make java.time values easier to work with we can add support for reader literals. For Clojure to be able to print the literals define the following methods.

(defmethod print-method java.time.Instant
  [inst out]
  (.write out (str "#time/inst \"" (.toString inst) "\"") ))

(defmethod print-dup java.time.Instant
  [inst out]
  (.write out (str "#time/inst \"" (.toString inst) "\"") ))

(defmethod print-method LocalDate
  [^LocalDate date ^FileWriter out]
  (.write out (str "#time/ld \"" (.toString date) "\"")))

(defmethod print-dup LocalDate
  [^LocalDate date ^FileWriter out]
  (.write out (str "#time/ld \"" (.toString date) "\"")))

To allow Clojure to read these literals first create two functions for passing date and time.

(defn parse-date [string]
  (LocalDate/parse string))

(defn parse-time [string]
  (and string (-> (.parse (DateTimeFormatter/ISO_INSTANT) string)
                  Instant/from)))

Then create a file called data_readers.clj in your project resources folder (this will add it to the class path).

{time/inst jdbc-java-time-example.core/parse-time
 time/ld   jdbc-java-time-example.core/parse-date}

Writing literals works as expected.

(jdbc/insert!
   database-connection
   :event
   {:name     "page_viewed"
    :created  #time/inst "2019-08-03T16:28:25.935Z"
    :log_date #time/ld "2019-08-03"})

=> ({:pid 1,
     :name "page_viewed",
     :created #time/inst "2019-08-03T16:28:25.935Z",
     :log_date #time/ld "2019-08-03"})

Reading prints literals.

(jdbc/query
   database-connection
   ["select * from event"])

=> ({:pid 1,
     :name "page_viewed",
     :created #time/inst "2019-08-03T16:28:25.935Z",
     :log_date #time/ld "2019-08-03"})

This concludes this guide to using java.time with clojure.java.jdbc as well as a brief introduction to reader literals. The full example project can be found here.