Search code examples
jsonclojureclojure-java-interop

how do I get a date stored in a database as a string in Clojure?


When getting values back out of a Postgres database with clojure.java.jdbc, I'm ending up with

{:id 1, :name "example", :created_at #object[java.time.LocalDateTime 0x15e0aadc "2019-02-08T12:52:11.438633"]}

when I want

{:id 1, :name "example", :created_at :created_at "2019-02-08T12:52:11.438633"]}

I want the date to be returned as JSON and consumable by JavaScript. The second example may not be the ideal format for that and so I'm open to others, but a Java object is not going to work.

I tried a couple of approaches, but the compiler produced a number of 'unmatched parenthesis' errors as a result of the date #object.

Thoughts on how to do this? Do I do something with the original SQL query, do I do something with my returned data, or something else?


Solution

  • I tried recreating your result with the H2 db, but it is giving me a Clojure #inst result instead of an object reference.


    OK, here is the problem. As your printout says, you have a java.time.LocalDateTime (a Clojure #inst is a java.util.Date object).

    If you want a string version, all you need to do is call the member function toString on it:

      (let [ldt (LocalDateTime/parse "2019-02-01T00:00:00" )]
    
        (.toString ldt) => <#java.lang.String "2019-02-01T00:00">
    

    However, you have no Time Zone information attached. So, you probably want a ZonedDateTime. WE WILL ASSUME UTC TIME ZONE BELOW (please verify for your DB server config):

      (let [jud      (Date.)
            ldt      (LocalDateTime/parse "2019-02-01T00:00:00")
            zdt      (.atZone ldt (ZoneId/of "UTC"))  ; *** big assumption here! ***
            inst     (.toInstant zdt)
            inst-str (.toString inst) ]
    
        ; a Clojure #inst is just a java.util.Date object
        jud      =>  #inst "2019-02-09T19:38:30.088-00:00"   
    
        ; a ZonedDateTime can print in 2 ways
        zdt              => #object[java.time.ZonedDateTime 0x780af31 "2019-02-01T00:00Z[UTC]"]
        (.toString zdt)  => "2019-02-01T00:00Z[UTC]"
    
        ; a java.time.Instant also prints in 2 ways:
        instant          => #object[java.time.Instant 0x48301adb "2019-02-01T00:00:00Z"]
        instant-str      => "2019-02-01T00:00:00Z"
    

    Note that the ZDT has a suffix like [UTC] tacked onto the end, so you may wish to convert it to an Instant and then use the .toString method to get a simpler string representation of it (ISO-8601).


    If you don't mind using an external library to make this easier, the tupelo.java-time lib has a helper function that is very handy:

    (ns xyz
      (require [tupelo.java-time :as tjt] ... ))
    
    (tjt/string-date-time-iso zdt) => "2019-02-01T00:00:00Z"
    

    There are many other helper functions available. Please see the API Docs and the unit tests for examples.


    Update

    I finally got my Postgres install fixed up (had to reset the password to make Hikari work). Here is my test code:

    (ns tst.demo.jdbc-pool
      (:use demo.core tupelo.core tupelo.test)
      (:require
        [clojure.java.jdbc :as jdbc]
        [hikari-cp.core :as pool]
        [tupelo.java-time :as tjt] ) )
    
    (def datasource-options-pg
      {:adapter       "postgresql"
       :database-name "alan"
       :server-name   "localhost"
       :port-number   5433
       :username      "alan"
       :password      "secret" } )
    
    (def ^:dynamic db-conn nil)
    
    (defn with-connection-pool
      "Creates and uses a connection for test function"
      [tst-fn]
      (let [datasource (pool/make-datasource datasource-options-pg)]
        (binding [db-conn {:datasource datasource}]
          (tst-fn)
          (pool/close-datasource datasource)))) ; close the connection - also closes/destroys the in-memory database
    
    (use-fixtures
      :once with-connection-pool) ; use the same db connection pool for all tests
    

    The above is all just config stuff. Here is the unit test that verifies the behavior:

    (dotest
      (jdbc/db-do-commands db-conn ["drop table if exists langs"])
      (jdbc/db-do-commands db-conn
        [(jdbc/create-table-ddl :langs [[:id :serial]
                                        [:lang "varchar not null"]
                                        [:creation :timestamptz]])])
      (jdbc/insert-multi! db-conn :langs
        [{:lang "Clojure" :creation (tjt/iso-str->timestamp "2008-01-01T12:34:56Z")}
         {:lang "Java"    :creation (tjt/iso-str->timestamp "1995-06-01T07:08:09Z")}])
    
      (let [result (jdbc/query db-conn ["select * from langs"])]
        (is=  (vec result)
          [{:id       1, :lang "Clojure",
            :creation #inst "2008-01-01T12:34:56.000000000-00:00"}
           {:id       2, :lang "Java",
            :creation #inst "1995-06-01T07:08:09.000000000-00:00"}])))
    

    So you can see I am still getting a java.util.Date result, which Clojure prints with the #inst formatting. I'm not sure how you are getting JDBC to output the LocalDateTime format.