renesca-magic is an abstraction layer for renesca, which generates typesafe graph database schemas for the Neo4j database using scala macros.
There is also an academic article about renesca and renesca-magic: An Open-Source Object-Graph-Mapping Framework for Neo4j and Scala: Renesca. Feel free to use these libraries in any of your projects. If you use them in an academic setting, we appreciate if you cite this article.
Dietze, F., Karoff, J., Calero Valdez, A. , Ziefle, M., Greven, C., & Schroeder, U. (2016, August). An Open-Source Object-Graph-Mapping Framework for Neo4j and Scala: Renesca. In International Conference on Availability, Reliability, and Security (pp. 204-218). Springer International Publishing.
- Generate boilerplate classes and factories to wrap Nodes, Relations and Graphs
- Generate boilerplate for handling HyperRelations
(n)-[]->(hyperRelation)-[]->(m)
- Generate getters, setters and factories for properties (primitives, optional primitives, default values)
- Generate accessors for neighbours on Nodes, over Relations and HyperRelations
- Node and Relation traits with multiple inheritance for labels and properties
- Generate filtered set accessors for Nodes, Relations and traits in a Graph
- Graph can inherit Nodes from multiple other Graphs
- View generated code in
/magic
of your sbt project root. (you should add it to your.gitignore
)
To use renesca-magic in your sbt project, add these dependencies and the marco compiler plugin to your build.sbt
:
libraryDependencies += "com.github.renesca" %% "renesca-magic" % "0.3.4-1"
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
Please don't hesitate to create issues about anything. Ideas, questions, bugs, feature requests, criticism, missing documentation, confusing examples, ... . Are you stuck with renesca or renesca-magic for some time? Is there something in this README that is unclear? Anything else? This means something does not work as intended or the API is not intuitive. Contact us and let's fix this together.
You can find all of these examples available as sbt project: renesca/renesca-magic-example. You can also have a look at the generated code: renesca-magic-example/magic
In renesca, nodes represent low level graph database entities from the property graph model. This is a bit annoying to work with when you have a schema in mind. For example, when we have types of nodes that have a specific label, we always access neighbours with another specific label. The same holds for properties for specific labels.
We can wrap our low-level nodes and relations in classes that take care of and hide the boilerplate. In the following example we provide some hand-written boilerplate to demonstrate this.
Here we have two nodes Animal
and Food
, connected with the relation Eats
. The nodes have getters and setters for their properties and accessors to their neighbours. There are also Factories to wrap low level entities or create new ones.
import renesca.graph._
import renesca.parameter._
import renesca.parameter.implicits._
case class Animal(node: Node) {
val label = Label("ANIMAL")
def eats: Set[Food] = node.outRelations.
filter(_.relationType == Eats.relationType).map(_.endNode).
filter(_.labels.contains(Food.label)).map(Food.wrap).toSet
def name: String = node.properties("name").asInstanceOf[StringPropertyValue]
}
object Animal {
val label = Label("ANIMAL")
def wrap(node: Node) = new Animal(node)
def create(name: String): Animal = {
val wrapped = wrap(Node.create(List(label)))
wrapped.node.properties.update("name", name)
wrapped
}
}
...
See the full boilerplate example at renesca-example/.../Schema.scala.
This is a lot of code for a single relation between two nodes. Writing this by hand for a larger schema takes a lot of time and is very error prone. We can use renesca-magic to generate this for us. Simply write:
import renesca.schema.macros
@macros.GraphSchema
object ExampleSchemaWrapping {
// Nodes get their class name as uppercase label
@Node class Animal { val name: String }
@Node class Food {
val name: String
var amount: Long
}
// Relations get their class name as uppercase relationType
@Relation class Eats(startNode: Animal, endNode: Food)
}
import ExampleSchemaWrapping._
val snake = Animal.create("snake")
val cake = Food.create(name = "cake", amount = 1000)
val eats = Eats.create(snake, cake)
cake.amount -= 100
Note that Food.name
is a val
and only generates a getter. Food.amount
is a var
and therefore generates a getter and a setter.
You can have a look at the generated code in the folder /magic
created at the root of your sbt project. Files created in /magic
are not needed for compilation. You can safely delete them and put the folder into your .gitignore
.
Use the @Graph
annotation to wrap a subgraph. This generates filtered Set accessors for each Node and Relation type.
import renesca.schema.macros
@macros.GraphSchema
object ExampleSchemaSubgraph {
@Node class Animal { val name: String }
@Node class Food {
val name: String
var amount: Long
}
@Relation class Eats(startNode: Animal, endNode: Food)
// Relations between specified nodes will be induced
@Graph trait Zoo { Nodes(Animal, Food) }
}
import ExampleSchemaSubgraph._
val zoo = Zoo(db.queryGraph("MATCH (a:ANIMAL)-[e:EATS]->(f:FOOD) RETURN a,e,f"))
val elefant = Animal.create("elefant")
val pizza = Food.create(name = "pizza", amount = 2)
zoo.add(Eats.create(elefant, pizza))
zoo.animals // Set(elefant)
zoo.relations // Set(elefant eats pizza)
db.persistChanges(zoo)
@macros.GraphSchema
object ExampleSchemaTraits {
// Inheriting Nodes receive their name as additional label
@Node trait Animal { val name: String }
// Node with labels FISH and ANIMAL
@Node class Fish extends Animal
@Node class Dog extends Animal
@Relation trait Consumes { val funny:Boolean }
// Relations can connect Node traits
// instead of defining relations for Fish and Dog explicitly
@Relation class Eats(startNode: Animal, endNode: Animal) extends Consumes
@Relation class Drinks(startNode: Animal, endNode: Animal) extends Consumes
// Zoo contains all Animals (Animal expands to all subNodes)
@Graph trait Zoo { Nodes(Animal) }
}
import ExampleSchemaTraits._
val zoo = Zoo.empty
// merge dog and fish on the name property
// (creates the animal if it does not exist, otherwise the existing animal is matched)
val bello = Dog.merge(name = "bello", merge = Set("name"))
val wanda = Fish.merge(name = "wanda", merge = Set("name"))
zoo.add(bello)
zoo.add(wanda)
zoo.animals // Set(bello, wanda)
// We can connect any node extending the trait Animal
zoo.add(Eats.create(bello, wanda, funny = false))
zoo.add(Drinks.create(wanda, bello, funny = true))
@macros.GraphSchema
object ExampleSchemaMultipleInheritance {
// Assignments are default values for properties
// They can also be arbitrary statements
@Node trait Uuid { val uuid: String = java.util.UUID.randomUUID.toString }
@Node trait Timestamp { val timestamp: Long = System.currentTimeMillis }
@Node trait Taggable
@Node class Article extends Uuid with Timestamp with Taggable {
val content:String
}
@Node class Tag extends Uuid { val name:String }
@Relation class Categorizes(startNode:Tag, endNode:Taggable)
@Graph trait Blog {Nodes(Article, Tag)}
}
import ExampleSchemaMultipleInheritance._
val initGraph = Blog.empty
initGraph.add(Tag.create(name = "useful"))
initGraph.add(Tag.create(name = "important"))
db.persistChanges(initGraph)
val blog = Blog.empty
// match the previously created tags
blog.add(Tag.matches(name = Some("useful"), matches = Set("name")))
blog.add(Tag.matches(name = Some("important"), matches = Set("name")))
// automatically set uuid and timestamp
val article = Article.create(content = "Some useful and important content")
blog.add(article)
// set all tags on the article
blog.tags.foreach{ tag =>
blog.add(Categorizes.create(tag, article))
}
blog.taggables // Set(article)
article.rev_categorizes // blog.tags
db.persistChanges(blog)
@macros.GraphSchema
object ExampleSchemaHyperRelations {
@Node trait Uuid { val uuid: String = java.util.UUID.randomUUID.toString }
@Node trait Taggable
@Node class Tag extends Uuid { val name:String }
@Node class User extends Uuid { val name:String }
@Node class Article extends Uuid with Taggable { val content:String }
// A HyperRelation is a node representing a relation:
// (n)-[]->(hyperRelation)-[]->(m)
// It behaves like node and relation at the same time
// and therefore can extend node and relation traits
@HyperRelation class Tags(startNode: Tag, endNode: Taggable) extends Uuid
// Because these are nodes, we can connect a HyperRelation with another Node
@Relation class Supports(startNode: User, endNode: Tags)
}
import ExampleSchemaHyperRelations._
val user = User.create(name="pumuckl")
val helpful = Tag.create(name="helpful")
val article = Article.create(content="Dog eats Snake")
val tags = Tags.create(helpful, article) // HyperRelation
val supports = Supports.create(user, tags) // Relation from user to HyperRelation
renesca-magic is free software released under the Apache License, Version 2.0