Статьи

Neo4j и спрей JSON

Название этого поста может быть немного запутанным — какое отношение 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где мы не хотимNode
  • findOneEntityWithIndexгде мы хотим посмотреть 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 появится в ближайшие несколько дней.