code-small.jpg

Cake Team Blogs

Spring Data Neo4j and MongoDB

Posted by Jan Machacek

Find me on:

03/04/12 18:22

Following from the last post on Scala and Spring Data with Neo4j, where I showed how to configure Spring Data to persist Neo4j instances. I showed code using Scala-ified Java code; in other words, the persistent class was defined as:

@NodeEntity
class User {
  @GraphId
  var id: java.lang.Long = _

  @Indexed(indexName = "username", indexType = IndexType.FULLTEXT)
  var username: String = _

  override def toString = {
    "User %d %s".format(id, username)
  }
}

Suppose now you'd like to take advantage of Scala's case classes. You may think of case classes as targets for the match statement to extract the values of the properties. Also, the case classes include generated equals and hashCode methods that work on the values in the first parameter list of the constructor. So, Suppose that our User class is fully identified by its id and username and we may want to use pattern matching to extract the values. We would need to define the class as:

@NodeEntity
case class User @PersistenceConstructor() (@GraphId id: java.lang.Long,
                                           @Indexed username: String) {

  def this() = this(null, null)
}

Now, this looks quite different. We have the case class User with a constructor annotated with the @PersistenceConstructor() annotation, with the two values that will become the fields in the class with the appropriate getters. If you ran this code, Spring Data would complain that the class User is missing a field with the @GraphId annotation. You see, the @GraphId annotation is on the constructor parameter, not on the field. We need to ensure that Scala propagates the annotation to the field as well.

To do so, we need to use the @annotation.target.field annotation on the constructor parameter. The only thing is the awkward syntax we'd have to use:

@NodeEntity
case class User @PersistenceConstructor() (
   @(GraphId @field) id: java.lang.Long,
   @(Indexed @field) username: String) {

  def this() = this(null, null)
}

Notice the @(GraphId @field) and @(Indexed @field). Yuck. A nice workaround is to define a new type that specifies the annotation with the @field annotation:

object annotations {
  type GraphId = org.springframework.data.neo4j.annotation.GraphId @field
  type Id = org.springframework.data.annotation.Id @field
  type Field =  org.springframework.data.mongodb.core.mapping.Field @field
  type Indexed = org.springframework.data.mongodb.core.index.Indexed @field
}

Notice that the newly defined types' names are the same; to use them, all we have to do is to import the types from the annotations object:

import org.springframework.data.annotation.PersistenceConstructor
import org.springframework.data.neo4j.annotation.{NodeEntity}
import org.springframework.data.mongodb.core.index.Indexed
import annotations._

@NodeEntity
case class User @PersistenceConstructor() (@GraphId id: java.lang.Long,
                                           @Indexed username: String) {

  def this() = this(null, null)

}

The last niggle is that we have to include the nullary constructor. I am looking at extending the AbstractConstructorEntityInstantiator<STATE> component of Spring Data to construct the @NodeEntity using the annotations on the constructor fields.

Naturally, if you want to include fields that you don't want to extract in pattern matching and that you don't want to include in the automatic equals and hashCode, there's nothing stopping you from including the usual vars:

import org.springframework.data.annotation.PersistenceConstructor
import org.springframework.data.neo4j.annotation.{NodeEntity}
import org.springframework.data.mongodb.core.index.Indexed
import annotations._

@NodeEntity
case class User @PersistenceConstructor() (@GraphId id: java.lang.Long,
                                           @Indexed username: String) {
  var firstName: String = _
  var lastName: String = _

  def this() = this(null, null)

}

Summary

So, with the annotated annotations, we can now take advantage of the case class machinery in Scala, and to connect it to the persistence mechanism provided automatically by Spring Data. Next, we need to take a look at cross-persistence, where we combine storage mechanism. What I mean by that is that it is possible to have an instance whose first two fields come from Neo4j and other two come from MongoDB. Automatically!, without having to write any code to make that happen in our projects.

Posts by Topic

see all

Subscribe to Email Updates