Datomic (Free)!
After hours of Googling and YouTube, I finally have Datomic working in my application! I’ll go over how to set up the free version locally and some areas where I got stuck.
Starting your Datomic Server
Download
The first thing that you’ll need to do is to download Datomic Free. You can go ahead and install the latest version (currently 0.9.5703.21) and be sure not to lose the download path–you’ll need that for this next part.
Once Datomic is downloaded and unzipped, you should see a couple files:
bin/transactor
config/samples/free-transactor-template.properties
Execute
Open the directory in your console and run the command:
- Mac:
bin/transactor config/samples/free-transactor-template.properties
- Windows:
.\bin\transactor config\samples\free-transactor-template.properties
After executing the command, you should see something like this in the console:
Starting datomic:free://localhost:4334/<DB-NAME>, storing data in: data ...
System started datomic:free://localhost:4334/<DB-NAME>, storing data in: data
Your connection URI (to be used later) is that
datomic:free://localhost:4334/<DB-NAME>
string. <DB-NAME>
should be replaced with
whatever you want to name your database.
Once you have the Datomic server up and running, you should be able to move this out of your install directory.
Adding Datomic to your Project
Creating a Schema
Before we start querying our Datomic server, we will need to define our database structure with a schema, which can be saved anywhere in the project’s folder.
Suppose we just want to be able to save people’s names and ages. All we need in our schema is this:
[{:db/ident :person/first
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :person/last
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :person/age
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one}]
Here’s a short breakdown of the schema definition:
- Each map is a column
:db/ident
is the name of the columnperson
is the table namefirst
,last
, andage
are the column names
:db/valueType
is the type of data the column holds- strings, integers, etc.
:db/cardinality
represents the relationship of the column to the record being added- one-to-one, one-to-many, many-to-one, many-to-many
For simplicity, everything is one-to-one in this example. You can read up on datomic schemas if you need more complex data structures.
Updating Dependencies
Now that we have the schema, we need to add a dependency to datomic-free
.
In project.clj
, add the latest version of datomic-free
to your
list of :dependencies
. This will allow you to reference datomic.api
in your source files.
(defproject
;...
:dependencies [;...
[com.datomic/datomic-free "0.9.5697"]]
;...
)
Connecting to Datomic
Next, we will need to create a connection to Datomic in our source file.
Be sure to require datomic.api
.
First, we’ll define our connection URI. You can use either an in-memory connection or your datomic server’s URI from the first step.
; Datomic Server
(def uri "datomic:free://localhost:4334/people")
; In-Memory
(def uri "datomic:mem://people")
You’ll also need to have your database schema handy, so be sure to define that as well.
(def schema (load-file "person_schema.edn"))
Now we need to make sure the database exists using create-database
. If it already exists,
then nothing will happen–your database and its data will remain unaffected.
After that, you can connect
to the database, load your schema
,
and return the connection to the caller.
(defn connect []
(create-database uri)
(let [conn (connect uri)]
(transact conn schema)
conn))
This connection will be used for each database call.
Saving Data
To add a record, just place the record into a vector and pass it into
the transact
function along with the database connection.
(defn save-person [conn first last age]
@(transact conn
[{:person/first first
:person/last last
:person/age age}]))
You can add as many records as you’d like in the transact
vector,
and they will all be added to the database.
I haven’t had any need for this, but if you record the value returned by this function, you’ll receive back something like this:
=> (save-person (connect) "Bill" "Murray" 71)
{:db-before datomic.db.Db@317d6b7
:db-after datomic.db.Db@551da7ce
:tx-data [#datom[13194139534313 50 #inst "2021-11-30T04:28:34.549-00:00" 13194139534313 true]
#datom[17592186045418 63 "Bill" 13194139534313 true]
#datom[17592186045418 64 "Murray" 13194139534313 true]
#datom[17592186045418 65 71 13194139534313 true]]
:tempids {-9223301668109597993 17592186045418}}
Retrieving Data
Now that we can save data, how do we get this data back for later use?
Well, will just use the q
function to query the db
(database) pointed to by the connection.
(defn find-all-people [conn]
(q '[:find ?person-first ?person-last ?person-age
:where [?eid :person/first ?person-first]
[?eid :person/last ?person-last]
[?eid :person/age ?person-age]]
(db conn)))
If you’re familiar with SQL, this is a lot like writing SELECT First, Last, Age from People
.
While this datalog query may look a little weird at first, once you become familiar with its
structure it will start to make more sense.
In the :where
clause, we can name each record property whatever we’d like.
Here I chose ?eid
, ?person-first
, ?person-last
, and ?person-age
.
The first, last, and age properties may look familiar. We just map these names over to
the :person/column-name
keywords. But where does ?eid
come from, and what is it?
When an entity is added to the database, it’s automatically given an ID that can be used for identifying each of a record’s parts. So here we are joining each part of the records by this ID.
In :find
, we select which items we want to return and in what order they should
appear in the result. If we ran this function with different :find
orders,
here’s what the results would be.
; :find ?person-first ?person-last ?person-age
=> (find-all-people (connect))
#{["Bill" "Murray" 71] ["Nick" "Offerman" 51] ["Harrison" "Ford" 79]}
; :find ?person-age ?eid ?person-first
=> (find-all-people (connect))
#{[71 17592186045418 "Bill"] [79 17592186045420 "Harrison"] [51 17592186045422 "Nick"]}
Closing Connections
There was one issue I ran into where my application would hang at the command prompt when it should have terminated. This has to do with database connections remaining open within the application.
While you could always do a (System/exit 0)
to close your application,
there is a proper way to close your database connections.
Using Datomic’s (shutdown true)
function, your connections will allow your
application to close gracefully once it has reached a termination point.
While this works from an integration perspective, I noticed while testing that
I was still able to perform queries using the connection initially created.
To close the connection, you can use Datomic’s release
function. However,
this must be called before the shutdown
function, otherwise you will
receive a nice little exception message about Clojure’s displeasure.
I ended up creating this function to handle open connections:
(defn disconnect [conn]
(release conn)
(shutdown true))
Now that you’ve got Datomic all set up, start building some crazy data structures and queries!