Kotan Code 枯淡コード

In search of simple, elegant code

Menu Close

Accessing Neo4j from Play Framework 2.1 with Web Service Futures

As you may have noticed, I’ve recently been doing a little bit of talking about Neo4j and graph databases. A database on its own is a fantastic thing, but it doesn’t do anybody much good if the information contained within stays there, or if you can’t put information into it from some application. In my own proof of concept, I wanted to see how easy it would be to access Neo4j from my favorite web application framework, Play.

Turns out it was a little annoying because I started looking for frameworks to talk to Neo4j. First I looked at AnormCypher, which is a library that provides an “anorm-like” API access to Cypher. I couldn’t use this one because it failed with multiple runtime errors when I attempted to bring it into my Play application. Next, I tried another Cypher wrapper that I found on github and that one failed miserably, too – it had a conflict between the version of a JSON parser it was using versus the one that my Play application was using.

Then I figured, why even bother? It’s just a REST API. So, I decided to try accessing Neo4j using nothing but Play framework’s own Web services API, which is basically the WS object. This turned out to be stupidly easy and, when I figured out future chaining in combination with that WS API, amazing stuff started to happen. You know, like, productivity.

First, I created a teeny little wrapper around the Neo4j REST API URL, I just called it NeoService:

class NeoService(rootUrl: String) {
    def this() = this("http://default/neo/URL/location/db/data")

    val stdHeaders = Seq( ("Accept", "application/json"), ("Content-Type", "application/json") )

    def executeCypher(query: String, params: JsObject) : Future[Response] = {
        WS.url(rootUrl + "/cypher".withHeaders(stdHeaders:_*).post(Json.obj(
            "query" -> query,
            "params" -> params
            ))
       )
    }

    // Convert a Future[Response] into a Future[Int]!
    def findNodeIdByKindAndName(kind:String, name:String) : Future[Option[Int]] = {
        val cypher = """
          START n=node:node_auto_index(kind={theKind})
          WHERE n.name = {theName}
          RETURN id(n) as id
       """.stripMargin
        val params = Json.obj( "theName" -> name, "theKind" -> kind )
        for (r <- executeCyper(cypher, params)) yield {
            val theData = (r.json \ "data").as[JsArray]
            if (theData.value.size == 0)
               None
            else
               // Json: "data" : [ [ ### ] ]
               Some(theData.value(0).as[JsArray].value(0).as[Int])
        }
    }
}

So, in this little tiny class I’ve got a function that I can use to execute Cypher queries and because I know that I have an auto-index’d node property called kind and I have another property called name, I can attempt to find a node’s ID based on the kind and name properties using a Cypher query. Instead of finding them synchronously, I can just convert my Future[Response]  into a Future[Option[Int]] where the Option[Int] is the result of looking through the Neo4j database for that data. I’ve just converted a future with a future, and being able to do so is pretty freaking awesome.

Not only can I do that, but I can chain these method calls from other code, like this:

val neo = new utils.NeoService()
for (nodeId <- neo.findNodeIdByKindAndName("zombie", "bob"))
    for (zombieId <- otherObject.tweakZombie(zombieId))
        yield {
           zombieId.map { id => println("I got a zombie!") }.getOrElse { println("Zombie doesn't exist!") }
        }

In the code here, tweakZombie can be written to be aware of the Option[] so that if it’s empty, it doesn’t tweak anything, allowing me to chain call after call after call and not worry about slapping a crapload of if statements in there – a judicious use of map, and for, and options and futures gives me a ridiculous amount of power.

All of this is made possible by the fact that the Play Framework Web Services API is based on Futures. I was originally skeptical of futures because the old code I had seen before was more confusing than single-threaded, synchronous programming. With the new Futures syntax and well-coded libraries like the Play WS object, you can do ridiculous things in a very small number of lines of code.