neo4j-clj

0.2.0-SNAPSHOT


Clojure bindings for Neo4j using the Java driver

dependencies

org.clojure/clojure
1.8.0
org.neo4j/neo4j
3.0.1
org.neo4j/neo4j-jdbc-driver
3.0.1



(this space intentionally left almost blank)
 

Neo4j communicates with Java via custom data structures. Those are can contain lists, maps, nulls, values or combinations. This namespace has functions to help to convert between Neo4j's data structures and Clojure

(ns neo4j-clj.compability
  (:import (org.neo4j.driver.v1 Values)
           (org.neo4j.driver.internal InternalRecord InternalPair
                                      InternalStatementResult)
           (org.neo4j.driver.internal.value NodeValue ScalarValueAdapter
                                            NullValue ListValue)
           (org.neo4j.cypher.internal.javacompat ExecutionResult)))

Convert to Neo4j

Neo4j expects a map of key/value pairs. The map has to be constructed as a Values.parameters instance which expects the values as an Object array

(defn clj->neo4j
  [val]
  (->> val
       clojure.walk/stringify-keys
       (mapcat identity)
       (into-array Object)
       Values/parameters))

Convert from Neo4j

Neo4j returns results as StatementResults, which contain InternalRecords, which contain InternalPairs etc. Therefore, this multimethod recursively calls itself with the extracted content of the data structure until we have values, lists or nil.

(defmulti neo4j->clj
  class)
(defmethod neo4j->clj InternalStatementResult [record]
  (map neo4j->clj (iterator-seq record)))
(defmethod neo4j->clj InternalRecord [record]
  (apply merge (map neo4j->clj (.fields record))))
(defmethod neo4j->clj InternalPair [pair]
  {(-> pair .key keyword) (-> pair .value neo4j->clj)})
(defmethod neo4j->clj NodeValue [value]
  (clojure.walk/keywordize-keys (into {} (.asMap value))))
(defmethod neo4j->clj ScalarValueAdapter [v]
  (.asObject v))
(defmethod neo4j->clj ListValue [l]
  (.asList l))
(defmethod neo4j->clj NullValue [n]
  nil)
 

This namespace contains the logic to connect to Neo4j instances, create and run queries as well as creating an in-memory database for testing.

(ns neo4j-clj.core
  (:require [neo4j-clj.compability :refer [neo4j->clj clj->neo4j]])
  (:import (org.neo4j.driver.v1 Values GraphDatabase AuthTokens)
           (org.neo4j.graphdb.factory GraphDatabaseSettings$BoltConnector
                                      GraphDatabaseFactory)
           (java.net ServerSocket)
           (java.io File)))

Returns a connection map from an url. Uses BOLT as the only communication protocol.

(defn create-connection
  ([url user password]
   (let [auth (AuthTokens/basic user password)
         db   (GraphDatabase/driver url auth)]
     {:url url, :user user, :password password, :db db}))
  ([url]
   (let [db (GraphDatabase/driver url)]
     {:url url, :db db})))
(defn- get-free-port []
  (.getLocalPort (ServerSocket. 0)))

In-memory databases need an uri to communicate with the bolt driver. Therefore, we need to get a free port.

(defn- create-temp-uri
  []
  (str "bolt://localhost:" (get-free-port)))

In order to store temporary large graphs, the embedded Neo4j database uses a directory and binds to an url. We use the temp directory for that.

(defn- in-memory-db
  [url]
  (let [bolt (GraphDatabaseSettings$BoltConnector. "0")
        temp (System/getProperty "java.io.tmpdir")]
    (-> (GraphDatabaseFactory.)
        (.newEmbeddedDatabaseBuilder (File. (str temp (System/currentTimeMillis))))
        ;; Configure db to use bolt
        (.setConfig (.type bolt) "BOLT")
        (.setConfig (.enabled bolt) "true")
        (.setConfig (.address bolt) url)
        (.newGraphDatabase))))

To make the local db visible under the same interface/map as remote databases, we connect to the local url. To be able to shutdown the local db, we merge a destroy function into the map that can be called after testing.

All data will be wiped after shutting down the db!

(defn create-in-memory-connection
  []
  (let [url (create-temp-uri)
        db (in-memory-db url)]
    (merge (create-connection url)
           {:destroy-fn (fn [] (.shutdown db))})))
(defn destroy-in-memory-connection [connection]
  ((:destroy-fn connection)))
(defn get-session [connection]
  (.session (:db connection)))
(defn- run-query [sess query params]
  (neo4j->clj (.run sess query params)))

Convenience function. Takes a cypher query as input, returns a function that takes a session (and parameter as a map, optionally) and return the query result as a map.

(defn create-query
  [cypher]
  (fn
    ([sess] (run-query sess cypher {}))
    ([sess params] (run-query sess cypher (clj->neo4j params)))))
 
(ns neo4j-clj.demo
  (:require [neo4j-clj.core :as db]))
(def create-user
  (db/create-query "CREATE (u:User {user})"))
(def get-all-users
  (db/create-query "MATCH (u:User) RETURN u as user"))
(def local-db (db/create-connection "bolt://localhost:7687" "neo4j" "password"))
(with-open [session (db/get-session local-db)]
  (create-user session {:user {:first-name "Luke" :last-name "Skywalker"}}))
(with-open [session (db/get-session local-db)]
  (get-all-users session))

=> ({:user {:first-name "Luke", :last-name "Skywalker"}})