Название этого поста может быть немного запутанным — какое отношение Neo4j имеет к Spray JSON? (Я знаю , что они оба работают на JVM, не пытайтесь быть умным ар * е .) Neo4j представляет собой базу данных графика и сделать его полезным для наших систем, вершина и ребра могут иметь свойство. Эти свойства могут быть String
s, числа, Date
s и тому подобное. Итак, как мы собираемся втиснуть богатые структуры в наших объектах в эти (скалярные) свойства?
Первоначальной реакцией может быть их сериализация ; как в, используйте 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
где мы не хотимNode
findOneEntityWithIndex
где мы хотим посмотретьNode
каким-то образомaddOne
который добавляет новыйNode
и добавляет его в индекс
Спрей JSON
Но мы пока не используем Spray JSON. Все , что я дал вам это какой — то механизм сортировочных между экземплярами некоторого типа A
и Neo4j Node
s. Давайте завершим картину, имея экземпляры класса типов :
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 появится в ближайшие несколько дней.