Saturday, February 25, 2012

Neo4J with Scala Play! 2.0 on Heroku (Part 7) :: DSM+DAO+Neo4J+Play


Note

This post is a continuation of this post, which is the sixth part of a blog suite that aims the use of Neo4j and Play2.0 together on Heroku.

Using Neo4J in Play 2.0... and simple DAO

What I'll intent to show is a way to use a Domain Specific Model, persisted in a Neo4J back end service. For such DSM, we'll have an abstract magic Model class that defines generic DAO operations.

For simplicity, we'll try to link each category of classes to the root/entry node. For instance, all the Users will be bound to the entry node by a reference of kind user.

Model

I'll choose the very common use case, that is, Users and Groups. Here is its shape:
  • A User has a first name
  • A Group has a name
  • A User can be in several Groups
  • A Group can contain several Users
  • A User can know several Users
Let's keep the classes definition aside for a few, and stick to the persistence service.

Graph Service

The Graph Service is an abstraction of what is needed for a Graph Persistence Layer. It is bound to a generic type that defines the model implementation and defines traversal and persistence operations of graph's nodes.
//A trait defining some high level operation on graph, hasn't been cleaned but illustrates well what it might be meant for
trait GraphService[Node] {
//entry point of the graph
def root: Node
//get a Node based on its id
def getNode[T <: Node](id: Int): Option[T]
//return all nodes in the graph as a list
def allNodes[T <: Node]: List[T]
//get the target of all relations that a node holds
def relationTargets[T <: Node](start: Node, rel: String): List[Node]
//save the given node in the graph
def saveNode[T <: Node](t: T): T
//index a node
def indexNode[T <: Node](model: T, indexName: String, key: String, value: String)
//create a relationship between two nodes
def createRelationship(start: Node, rel: String, end: Node)
}

Graph Service for Neo4J

Let's update now, the service that has been used in the previous post, for Neo4J persistence, in order to have it able to deal with model instance.

Let's start with the saveNode operation to see what is needed in the model and elsewhere.
trait Neo4JRestService extends GraphService[Model[_]] {
def saveNode[T <: Model[_]](t: T): T = {
//call the Neo4J Rest end point with the model instance formatted in Json
// (A) ==> needs a Json Formatter for the Model instance
//recover the id from the self url
// ==> ok
//set the 'id' property
// ==> ok
//link the instance to the entry node
// (B) ==> needs a way to associate a Class to a relation kind
//retrieve the resulting instance from Neo4J as Json unmarshalled in a related Model instance
// (C) ==> needs a Unformatter for the Model instance
//return the instance
}
}

In this Gist above, I've enlighted some points that must be found around the Model construction. (A) and (C) are composing a Json Format (as SJson propose), (B) is more related to model abstraction.

(C) has a special need when used with Dispatch, we could have a Dispatch Handler that can do both action parsing/unmarshalling and direct use in a continuation.

Model

Now, we are at the right point to talk the Model, since we've met almost all its requirement. So let's build a Magic Model class that can be extended by all concrete model classes.
Skeleton
That's the easy part, we just define the id property that is an id (part of the Rest Url in Neo4J).
Formatter
Ok, this part is simple too in this abstract Model definition because, a Format implementation must be part of the concrete DSM classes. That is, User that extends Model must define a Format[User] instance, and put it in the implicit context.
So, at this stage we have Model and User like this:
//Model definition
abstract class Model[A <: Model[A]] {
val id:Int;
}
//Concrete class
case class User(id: Int, firstName: String) extends Model[User] {
}
//companion object that defines the Format in the implicit scope
object User {
implicit object UserFormat extends Format[User] {
def reads(json: JsValue): User = User(
(json \ "id").asOpt[Int].getOrElse(null.asInstanceOf[Int]),
(json \ "firstName").as[String]
)
def writes(u: User): JsValue =
JsObject(List(
"_class_" -> JsString(User.getClass.getName),
"firstName" -> JsString(u.firstName)
) ::: (if (u.id != null.asInstanceOf[Int]) {
List("id" -> JsNumber(u.id))
} else {
Nil
}))
}
}

Class -- Relation's kind : F-Bounded
As we saw in the saveNode method needs to associate the concrete class to a relation kind. But what I wanted is to have a save method in Model, that implies that we cannot (at first glance) give the saveNode the information needed, that is the concrete class.

For that, we'll use a F-Bounded type for Model, that way we'll be able to give the saveNode method what is the really class... Mmmh ok, let me show you:
abstract class Model[A <: Model[A]] { // weird construction Mmmh... but it's very useful to have Model knowing who is currently extending it.
val id:Int;
//Now save can take two implicits
// m which is the class manifest of A, where A will be User for instance (see below)
// f which is the json formatter of the concrete class (again User discussed before)
def save(implicit m:ClassManifest[A], f:Format[A]):A = graph.saveNode[A](this.asInstanceOf[A])
}
case class User(id: Int, firstName: String) extends Model[User] { //here we see that we use the F-Bounded to define User as a Model of type User...)
//HERE IS THE GAIN : we don't have to redefine save, because of the extension.
}
But that's not sufficient, the saveNode method will need to use such available ClassManifest to find the relation it must create.

I choose a very common and easy solution, which is having a function in the Model companion that helps in registering classes against relation kind.
object Model {
// a mutable map that holds the look up table between class manifest and relation kind (String)
val models:mutable.Map[String, ClassManifest[_ <: Model[_]]] = new mutable.HashMap[String, ClassManifest[_ <: Model[_]]]()
//this helps the registration
// in Play, we can use the GlobalSetttings hook to register definitions (as Hibernate does f.i.)
def register[T <: Model[_]](kind:String)(implicit m:ClassManifest[T]) {
models.put(kind, m)
}
//determines the relation kind of a class
def kindOf[T <: Model[_]] (implicit m:ClassManifest[T]):String = models.find(_._2.equals(m)).get._1
}

Model Dispatch Handler

Now we'll discuss something I find really useful and easy in Dispatch, create a Handler that can handle a Json response from Neo4J into a Model instance.
For that, we have already defined in previous post a way to handle json response into Play's JsValue.

Now, what we need is to use the implicit formatter of all model concrete classes to create instances. And it'll be the way to reach the goal, except that a problem comes from the Json response of Neo4J: the data is not present at the Json root, but is the value of the data property.
So it breaks our Format if we use it directly.

That's why the above definition of the Handler takes an extra parameter which is the conversion between JsValue to JsValue, that is to say, a function that goes directly to the data definition.
class ModelHandlers(subject: HandlerVerbs) {
//Process response as Model Instance in block.
// Beware of the filter function that is meant to keep only the relevant data for our Model
def >^>[M <: Model[_], T](block: (M) => T)(implicit fmt: Format[M], filter: (JsValue) => JsValue = (j: JsValue) => j) = new PlayJsonHandlers(subject) >! {
(jsValue) =>
block(fromJson[M](filter(jsValue))) // Here we use the formatter in the implicits and the filter to match its needs.
}
// a handler for several result at once -- the filter method is meant to take a JsValue and to return an Iterable of JsValue
def >^*>[M <: Model[_], T](block: (Iterable[M]) => T)(implicit fmt: Format[M], filter: (JsValue) => Iterable[JsValue] = (j: JsValue) => Seq(j)) = new PlayJsonHandlers(subject) >! {
(jsValue) =>
block(filter(jsValue).map(fromJson[M](_)))
}
}

saveNode

Finally, let's gather all our work in a simple implementation of a generic saveNode function:
def saveNode[T <: Model[_]](t: T)(implicit m: ClassManifest[T], f: Format[T]): T = {
////////uses the Format for outputting the model instance to Json////////
val (id: Int, property: String) = Http(
(neoRestNode <<(stringify(toJson(t)), "application/json"))
<:< Map("Accept" -> "application/json")
>! {
jsValue =>
val id: Int = selfRestUriToId((jsValue \ "self").as[String])
(id, (jsValue \ "property").as[String])
}
)
//update the id property
Http(
(url(property.replace("{key}", "id")) <<(id.toString, "application/json") PUT)
<:< Map("Accept" -> "application/json") >| //no content
)
//////check below///////////
val model = getNode[T](id).get
//create the rel for the kind
linkToRoot(Model.kindOf[T], model) //////// get the relation kind to create //////////
model
}
//WARN :: the name conforms is mandatory to avoid conflicts with Predef.conforms for implicits
// see https://issues.scala-lang.org/browse/SI-2811
// This filter will simply keep the data property of the Neo4J response
implicit def conforms: (JsValue) => JsValue = {
(_: JsValue) \ "data"
}
def getNode[T <: Model[_]](id: Int)(implicit m: ClassManifest[T], f: Format[T]): Option[T] = {
////////// see how we simply get the Model instance by using our handler and the implicit filter (conforms) defined above ////////////
Http(neoRestNodeById(id) <:< Map("Accept" -> "application/json") >^> (Some(_: T)))
}
view raw saveNode.scala hosted with ❤ by GitHub
As we can see, it's very easy to handle Neo4J response as DSO and use them directly in the continuation method of the Handler.

Usage

having all pieces in places (check out the related Git repo here). We can now really simply create a User and retrieve it updated with its id, or even get it from the database using its id.
class UserSpec extends Specification {
var userId:Int = 0;
def is =
"Persist User" ^ {
"is save with an id" ! {
running(FakeApplication()) {
val user:User = User(null.asInstanceOf[Int], "I'm you")
val saved: User = user.save
userId = saved.id
saved.id must beGreaterThanOrEqualTo(0)
}
} ^
"can e retrieved easily" ! {
running(FakeApplication()) {
val retrieved = graph.getNode[User](userId)
retrieved must beSome[User]
}
}
}
}
view raw spec.scala hosted with ❤ by GitHub


In the next Post, we'll create some Play template for viewing such data, but create them also.

No comments:

Post a Comment