Database Integration with JDBC and Datahike

Choose between relational SQL access and immutable Datalog modeling, and learn the transaction, query, and schema trade-offs each approach brings in Clojure.

Database integration in Clojure is easiest when you decide early whether the problem is relational or fact-oriented. JDBC is the right fit when you have tables, joins, transactions, and SQL-shaped reporting. Datahike is the better fit when the domain is naturally expressed as immutable facts, references, and declarative Datalog queries. Both can work well, but they solve different modeling problems.

Choose the Database Model Before the Library

JDBC is not “the relational version” of Datahike. The two styles encourage different habits.

  • Relational access through JDBC is good for explicit schemas, row-oriented updates, and operational systems with mature SQL tooling.
  • Datahike is good for identity-rich data, graph traversal, temporal thinking, and query patterns that benefit from Datalog.

If your core question is “which rows should be inserted or updated in this transaction?”, JDBC is usually the better tool. If your core question is “what facts are true about these entities and how do they relate?”, Datahike becomes much more attractive.

    flowchart LR
	    APP["Clojure application"] --> DECIDE{"Model shape?"}
	    DECIDE -->|Tables, joins, row updates| JDBC["JDBC / SQL path"]
	    DECIDE -->|Facts, identities, graph relations| DATAHIKE["Datahike / Datalog path"]
	    JDBC --> SQLDB["Relational database"]
	    DATAHIKE --> FACTDB["Immutable fact store"]

JDBC in Clojure: Keep SQL Explicit

A healthy JDBC layer usually does four things well:

  • keeps SQL visible instead of hiding everything behind abstractions
  • parameterizes values safely
  • uses pooled connections
  • scopes transactions around real consistency needs
 1(ns myapp.users
 2  (:require [clojure.java.jdbc :as jdbc]))
 3
 4(def db-spec
 5  {:dbtype "postgresql"
 6   :dbname "app"
 7   :host "localhost"
 8   :port 5432
 9   :user "app_user"
10   :password "secret"})
11
12(defn find-user-by-email [email]
13  (first
14   (jdbc/query db-spec
15               ["select id, email, status
16                 from users
17                 where email = ?" email])))
18
19(defn create-user! [{:keys [email status]}]
20  (jdbc/insert! db-spec :users
21                {:email email
22                 :status status}))

The important part is the vector form for parameterized SQL. It prevents unsafe string interpolation and keeps queries readable.

Transactions Should Protect Business Invariants

Wrap a transaction around operations that must succeed or fail together, not around every query by habit.

 1(defn transfer-credit! [from-id to-id amount]
 2  (jdbc/with-db-transaction [tx db-spec]
 3    (jdbc/execute! tx
 4                   ["update accounts
 5                     set balance = balance - ?
 6                     where id = ?" amount from-id])
 7    (jdbc/execute! tx
 8                   ["update accounts
 9                     set balance = balance + ?
10                     where id = ?" amount to-id])))

This is where relational databases are strongest: multi-row consistency with explicit transactional semantics.

Datahike: Facts, Identity, and Datalog

Datahike stores facts as immutable assertions. Instead of mutating a row in place, you transact new facts into the database value. Queries are declarative and identity-driven.

 1(ns myapp.people
 2  (:require [datahike.api :as d]))
 3
 4(def cfg
 5  {:store {:backend :mem
 6           :id "people-db"}
 7   :schema-flexibility :write})
 8
 9(def schema
10  [{:db/ident :person/email
11    :db/valueType :db.type/string
12    :db/cardinality :db.cardinality/one
13    :db/unique :db.unique/identity}
14   {:db/ident :person/name
15    :db/valueType :db.type/string
16    :db/cardinality :db.cardinality/one}
17   {:db/ident :person/friend
18    :db/valueType :db.type/ref
19    :db/cardinality :db.cardinality/many}])
20
21(defn connect! []
22  (when-not (d/database-exists? cfg)
23    (d/create-database cfg))
24  (let [conn (d/connect cfg)]
25    (d/transact conn {:tx-data schema})
26    conn))

The connection setup matters because schema is part of the model, not an afterthought. Once connected, you transact entities as data:

 1(defn add-person! [conn {:keys [email name]}]
 2  (d/transact conn
 3              {:tx-data [{:person/email email
 4                          :person/name name}]}))
 5
 6(defn people-named [conn target-name]
 7  (d/q '[:find ?email
 8         :in $ ?target-name
 9         :where
10         [?e :person/name ?target-name]
11         [?e :person/email ?email]]
12       (d/db conn)
13       target-name))

The main conceptual shift is that the database is queried as a set of facts, not as mutable rows with foreign keys and joins written manually each time.

Practical Trade-Offs

JDBC Strengths

  • mature SQL ecosystem
  • strong transactional semantics
  • straightforward operational reporting
  • fits teams already fluent in relational design

Datahike Strengths

  • flexible relationship modeling
  • immutable database values
  • good fit for identity-rich and graph-like domains
  • declarative querying over facts and references

JDBC Risks

  • over-abstracting SQL until queries become opaque
  • leaking transaction scope across too much application code
  • turning every database access into ad hoc string building

Datahike Risks

  • weak relational instincts carried over into a fact model
  • unclear schema discipline
  • assuming Datalog is automatically simpler than SQL
  • underestimating the modeling shift required by immutable facts

Design Review Questions

Before picking one path, ask:

  1. Is the domain more naturally row-oriented or fact-oriented?
  2. Are multi-step relational transactions central to correctness?
  3. Will graph traversal and identity-based querying be common?
  4. Is the team more fluent in SQL or Datalog?
  5. Does the system need immutable historical database values as a first-class concept?

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026