Название этого поста может быть немного запутанным — какое отношение Neo4j имеет к Spray JSON? (Я знаю , что они оба работают на JVM, не пытайтесь быть умным ар * е .) Neo4j представляет собой базу данных графика и сделать его полезным для наших систем, вершина и ребра могут иметь свойство. Эти свойства могут быть Strings, числа, Dates и тому подобное. Итак, как мы собираемся втиснуть богатые структуры в наших объектах в эти (скалярные) свойства?
Первоначальной реакцией может быть их сериализация ; как в, используйте Object[Input|Output]Stream. Проблема с сериализацией Java заключается в том, что она довольно темпераментна. Все, что нужно, это добавить недвижимость и бум! , у вас проблемы с чтением объектов обратно. Было бы неплохо сохранить эти объекты в немного более управляемом формате. JSON довольно хорошо подходит, особенно когда у нас так много разных библиотек сериализации и десериализации JSON.
Если вы находитесь в проекте Spray , вы, вероятно, используете Spray JSON. Было бы полезно повторно использовать различные JsonFormat[A]экземпляры классов типов, чтобы не только иметь дело с JSON на уровне API, но и использовать тот же формат JSON в Neo4j. Итак, давайте перейдем к этому.
Низкоуровневый доступ
Начнем с низкоуровневого кода Neo4j. Нам понадобится код, который дает нам доступ к базовому GraphDatabaseService, создает узлы, поддерживает транзакции и другой программный код.
trait GraphDatabase {
def graphDatabase: GraphDatabaseService
/**
* Performs block ``f`` within a transaction
*
* @param f the block to be performed
* @tparam T the type the inner block returns
* @return ``f``'s result
*/
def withTransaction[T](f: => T): T = {
val tx = graphDatabase.beginTx
try {
val result = f
tx.success()
result
} catch {
case e: Throwable =>
tx.failure()
throw e
} finally {
tx.finish()
}
}
/**
* Creates a new and empty node
*
* @return the newly created node
*/
def newNode(): Node = graphDatabase.createNode()
}
Пока что больших сюрпризов нет. withTransactionФункция является вариацией , bracketи я даже не буду оскорблять ваш интеллект, описывая то , что newNode()делает функция. Но мы в Скале, и у нас есть эти сильные типы. Давайте посмотрим, как мы можем максимально использовать это.
/**
* Modifies the given ``Node``s with values in the instances of ``A``
*
* @tparam A the A
*/
trait NodeMarshaller[A] {
def marshal(node: Node)(a: A): Node
}
/**
* Unmarshals given ``Node``s to create instances of ``A``
*
* @tparam A the A
*/
trait NodeUnmarshaller[A] {
def unmarshal(node: Node): A
}
/**
* Provides index for the ``A``s
*
* @tparam A the A
*/
trait IndexSource[A] {
def getIndex(graphDatabase: GraphDatabaseService): Index[Node]
}
Сначала мы определяем классы типов, которые содержат функции для маршалирования значения типа Aв aNode , немаршализованного значения типа Aиз aNode и, наконец, для предоставления Indexнекоторого типаA . В Scala-говорят, это обычные черты в списке выше. Теперь давайте использовать их:
trait TypedGraphDatabase extends GraphDatabase {
type Identifiable = { def id: UUID }
import language.reflectiveCalls
private def createNode[A](a: A)
(implicit ma: NodeMarshaller[A]): Node =
ma.marshal(newNode())(a)
private def find[A](indexOperation: Index[Node] => IndexHits[Node])
(implicit is: IndexSource[A], uma: NodeUnmarshaller[A]):
Option[(A, Node)] = {
val index = is.getIndex(graphDatabase)
val hits = indexOperation(index)
val result = if (hits.size() == 1) {
val node = hits.getSingle
Some((uma.unmarshal(node), node))
} else {
None
}
hits.close()
result
}
private def byIdIndexOpertaion(id: UUID):
Index[Node] => IndexHits[Node] = index => index.get("id", id.toString)
def findOne[A <: Identifiable]
(id: UUID)
(implicit is: IndexSource[A], uma: NodeUnmarshaller[A]):
Option[(A, Node)] =
find(byIdIndexOpertaion(id))
def findOneEntity[A <: Identifiable]
(id: UUID)
(implicit is: IndexSource[A], uma: NodeUnmarshaller[A]):
Option[A] =
find(byIdIndexOpertaion(id)).map(_._1)
def findOneEntityWithIndex[A]
(indexOperation: Index[Node] => IndexHits[Node])
(implicit is: IndexSource[A], uma: NodeUnmarshaller[A]):
Option[A] =
find(indexOperation).map(_._1)
def addOne[A <: Identifiable]
(a: A)
(implicit is: IndexSource[A], ma: NodeMarshaller[A]): Node = {
val node = createNode(a)
is.getIndex(graphDatabase).putIfAbsent(node, "id", a.id.toString)
node
}
}
Здесь я показываю только самые важные функции, а именно:
findOneнаходит одну сущность и узел для данной идентичностиfindOneEntityкак удобный вариант,findOneгде мы не хотимNodefindOneEntityWithIndexгде мы хотим посмотретьNodeкаким-то образомaddOneкоторый добавляет новыйNodeи добавляет его в индекс
Спрей JSON
Но мы пока не используем Spray JSON. Все , что я дал вам это какой — то механизм сортировочных между экземплярами некоторого типа Aи Neo4j Nodes. Давайте завершим картину, имея экземпляры класса типов :
trait SprayJsonNodeMarshalling {
implicit def sprayJsonNodeMarshaller[A : JsonFormat] =
new SprayJsonStringNodeMarshaller[A]
implicit def sprayJsonNodeUnmarshaller[A : JsonFormat] =
new SprayJsonStringNodeUnmarshaller[A]
class SprayJsonStringNodeMarshaller[A : JsonFormat] extends
NodeMarshaller[A] {
def marshal(node: Node)(a: A) = {
val formatter = implicitly[JsonFormat[A]]
val json = formatter.write(a).compactPrint
node.setProperty("json", json)
node
}
}
class SprayJsonStringNodeUnmarshaller[A : JsonFormat] extends
NodeUnmarshaller[A] {
def unmarshal(node: Node) = {
val json = node.getProperty("json").toString
val parsed = JsonParser.apply(json)
val formatter = implicitly[JsonFormat[A]]
formatter.read(parsed)
}
}
}
Теперь это лучше. Если мы смешиваем TypedGraphDatabaseс SprayJsonNodeMarshallingи имеем экземпляры JsonFormatдля типов, которые мы сохраняем, мы в деле!
использование
Verba docent, например , tranunt , так что давайте посмотрим пример кода. Мы будем сохранять некоторые Customerэкземпляры. Давайте начнем с определения типа Customerданных.
case class Customer( id: UUID, firstName: String, lastName: String, dateOfBirth: Date, theNameOfTheHospitalInWhichHisMotherWasBorn: String)
Если мы теперь хотим использовать его с Spray JSON, нам нужен JsonFormatэкземпляр для типа Customer, и поскольку мы будем использовать этот экземпляр как в API, так и в доступе Neo4j, мы поместим его в черту, которую мы можем смешивать, где нам нужно. Это.
trait CustomerFormats extends DefaultJsonProtocol with
UuidFormats with DateFormats {
implicit val CustomerFormat = jsonFormat5(Customer)
}
Эти противные UuidFormatsи DateFormatsчерты определяют JsonFormatэкземпляры для типов UUIDи Date. Теперь мы почти готовы начать манипулировать Customerмагазином Neo4j. Последнее, что нам нужно, это определить, Indexв Customerкаких узлах-переносчики будут жить. Это работа IndexSource. Это немного более сложным
Нам нужно получить базовый GraphDatabaseServiceдля создания (или получить) Index.
trait CustomerGraphDatabaseIndexes {
this: GraphDatabase =>
lazy val customerIndex = graphDatabase.index().forNodes("customers")
implicit object CustomerIndexSource extends IndexSource[Customer] {
def getIndex(graphDatabase: GraphDatabaseService) = customerIndex
}
}
Теперь у нас есть все готовые компоненты: мы можем смешать в нашем примере приложения
object Demo extends
Application with
TypedGraphDatabase with
SprayJsonNodeMarshalling with
CustomerGraphDatabaseIndexes with
CustomerFormats {
// satisfy GraphDatabase.graphDatabase
val graphDatabase = new GraphDatabaseFactory().newEmbeddedDatabase("path")
val customer = Customer(...)
withTransaction {
addOne(customer)
}
val found = findOneEntity[Customer](customer.id)
}
Код сейчас на самом деле довольно приятный. У нас есть ORM для бедного человека на вершине графовой базы данных. Мы можем сохранять сложные экземпляры в свойствах наших узлов.
Резюме
Если вам нравится этот пост, следите за https://github.com/janm399/scalad ; функциональность Neo4j появится в ближайшие несколько дней.
