- Notifications
You must be signed in to change notification settings - Fork7
An Object Graph Mapping Library For Gremlin
License
karthicks/gremlin-ogm
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Karthick Sankaracharyhttp://github.com/karthicks
Thegremlin-objects
module defines a library that puts an object-oriented spin on the gremlin property graph.It aims to make it much easier to specify businessdomain specific languages
around Gremlin, without any loss of expressive power.While it targets the Gremlin-Java variant, the concept itself is language-independent.
Every element in the property graph, whether it be a vertex (property) or an edge, is made up of properties.Each such property is aString
key and an arbitraryJava
value.It only seems fitting then to try and represent that property as a strongly-typedJava
field.The specific class in which that field is defined then becomes the vertex (property) or edge, which the property describes.Agremlin object model
such as this would need abstractions to query and update the graph in terms of those objects.To get the library that facilitates all of this, add this dependency to yourpom.xml
:
<dependency> <groupId>com.github.karthicks</groupId> <artifactId>gremlin-objects</artifactId> <version>3.3.1-RC1</version></dependency>
A reference use case of this library is available in the followingtinkergraph-test
module:
<dependency> <groupId>com.github.karthicks</groupId> <artifactId>tinkergraph-test</artifactId> <version>3.3.1-RC1</version></dependency>
In this section, we go over how gremlin elements may be modeled, and how those models may be queried and stored.
Let’s consider the example of theperson
vertex, taken from the "modern" and "the crew" graphs defined in theTinkerFactory.In our object world, it would be defined as aPerson class that extendsVertex.By default, the vertex’s label matches its simple class name, hence we have to un-capitalize it using the@Alias annotation.
The person’sname
andage
properties become primitive fields in the class.The@PrimaryKey and@OrderingKey annotations on them not only indicate that they are mandatory,but also allow theperson
to be found easily through theHasKeys.of(person)SubTraversal
.Think of theSubTraversal as a reusable function that takes aGraphTraversal
, performs a few steps on it, and returns it back (to allow for chaining).TheKnowsPeople
field in this class is an example of an in-lineSubTraversal
, albeit a stronger-typed version of it calledToVertex
, to indicate that it ends up selecting vertices.Note that these traversal functions are not stored in the graph.
@Data@Alias(label ="person")publicclassPersonextendsVertex {publicstaticToVertexKnowsPeople =traversal ->traversal .out(Label.of(Knows.class)) .hasLabel(Label.of(Person.class));@PrimaryKeyprivateStringname;@OrderingKeyprivateintage;privateSet<String>titles;privateList<Location>locations;}
Next, we look at itstitles
field, which is defined to be aSet
.As you might expect, the cardinality of the underlying property becomesset
.Similarly, thelocations
field takes on thelist
cardinality.Further, each element in thelocations
list has it’s own meta-properties, and ergo deserves aLocation class of it’s own.
@Data@Alias(label ='location')publicclassLocationextendsElement {@OrderingKey@PropertyValueprivateStringname;@OrderingKeyprivateInstantstartTime;privateInstantendTime;}
Note | The value of thelocation is stored inname , due to the placement of the@PropertyValue annotation.Every other field in theLocation class becomes the `location’s meta-property. |
An edge is defined much like the vertex, except it extends theEdge class.By default, an edge’s label is it’s un-capitalized simple class name, and hence no@Alias
is needed:
@DatapublicclassKnowsextendsEdge {privateDoubleweight;privateInstantsince;}
TheGraph interface lets you update the graph usingVertex
orEdge
objects.You can get it via dependency injection, assuming you’ve anObject
provider forGraphTraversalSource
:
@Inject@ObjectprivateGraphgraph;
Or, the good old fashioned way, using theGraphFactory:
privateGraphFactorygraphFactory =GraphFactory.of(TinkerGraph.open().traversal());// This gets you the factory for TinkerGraph.privateGraph =graphFactory.graph();
Now that we know how to obtain aGraph
instance, let’s see how to change it usingJava
objects.Here, we createsoftware
vertices fortinkergraph
andgremlin
, and add atraverses
edge fromgremlin
totinkergraph
.
graph .addVertex(Software.of("tinkergraph")).as("tinkergraph") .addVertex(Software.of("gremlin")).as("gremlin") .addEdge(Traverses.of(),"tinkergraph");
Below, aperson
vertex containing a list oflocations
is added, along with three outgoing edges.
graph .addVertex(Person.of("marko",Location.of("san diego",1997,2001),Location.of("santa cruz",2001,2004),Location.of("brussels",2004,2005),Location.of("santa fe",2005))).as("marko") .addEdge(Develops.of(2010),"tinkergraph") .addEdge(Uses.of(Proficient),"gremlin") .addEdge(Uses.of(Expert),"tinkergraph")
To see how themodern
and thecrew
reference graphs may be created using the objectGraph
interface, gohere.
Tip | Since the object being added may already exist in the graph, we providevarious options to resolve "merge conflicts", such asMERGE ,REPLACE ,CREATE ,IGNORE ANDINSERT . |
There are two ways to get a handle to theQuery interface.You can inject it like so:
@Inject@ObjectprivateQueryquery;
Otherwise, you can create it using theGraphFactory
like so:
privateGraphFactorygraphFactory =GraphFactory.of(TinkerGraph.open().traversal());privateQuery =graphFactory.query();
Next, let’s see how to use theQuery
interface.The following snippet queries the graph by chaining twoSubTraversals
(a function denoting a partial traversal), and parses the result into a list ofPerson
vertices.
List<Person>friends =query .by(HasKeys.of(modern.marko),Person.KnowsPeople) .list(Person.class);
Below, we query by anAnyTraversal (a function on theGraphTraversalSource
), and get a singlePerson
back.
Personmarko =Person.of("marko");Personactual =query .by(g ->g.V().hasLabel(marko.label()).has("name",marko.name())) .one(Person.class);
The type of the result may be primitives too, and that is handled as shown below.
longcount =query .by(HasKeys.of(crew.marko),Count.of()) .one(Long.class);
Last, we show a traversal involving select steps, which requires special handling as it may return a map.
Selectionsselections =query .by(g ->g.V().as("a").properties("locations").as("b").hasNot("endTime").as("c").order().by("startTime").select("a","b","c").by("name").by(T.value).by("startTime").dedup()) .as("a",String.class) .as("b",String.class) .as("c",Instant.class) .select();
To see more examples showcasing how the objectQuery
interface may be used, gohere.
In this section, we talk about how thegremlin-objects
library can be customized for agraph system
provider.
A provider that wishes to plug intogremlin-objects
through dependency injection, will need to provide aGraphTraversalSource
of it’s choice, through theObject
qualifier.For users that don’t use dependency injection, they may manually pass theGraphTraversalSource
to theGraphFactory.
Typically, gremlin property values are Java primitives.Sometimes, a provider treats a custom type as a primitive.For instance,DataStax
lets you define property keys of the primitive geometric typePoint
.Such types can be registered using thePrimitives#registerPrimitiveClass
methods.
When aGraphTraversal
is completed, it usually returns (a list of) gremlinElement(s)
.However, when some providers execute a traversal, the result comprises custom element types.For instance, whenDataStax
executes a graph query, it returns a result set made up ofGraphNode(s)
, a proprietary element type.We give such providers a way to tell us how to parse such custom elements using theParsers#registerElementParser
method.
While there exist similar OGM libraries, this one has some key differentiating factors. Now, let’s consider the alternatives:
Thegremlin-core
module defines aGremlinDsl annotation that lets you define custom traversals by extending theGraphTraversal
andGraphTraversalSource
.However, it requires some familiarity ofgremlin-core
internals.
Peopod represents elements as annotated interfaces or abstract classes.While it generates boilerplate for traversals to adjacent vertices, it doesn’t let you co-locate arbitrary traversals.This library is less intrusive and more flexible.
An older version of TinkerPop allowed you to define custom steps usingClosures
, not unlike theAnyTraversal
andSubTraversal
functions.However, they aren’t as developer friendly as the functional interfaces provided here.Moreover, it doesn’t allow for co-locating the traversal logic along with the element model, as we do here.
So far, we have thegremlin-objects
library, and atinkergraph-test
reference use case for it.Here, we list a few directions in which we see the library evolving:
The concept of lifting the property graph into objects is language-independent.To quote the TinkerPop docs, "with JSR-223, any language compiler written for the JVM can directly access the JVM and any of its libraries", and that would includegremlin-objects
.For GLVs not written for the JVM, it can be ported over as long as it supports basic reflection.Case in point, the Gremlin-Python variant could achieve the object mapping through thedir,getattr andsetattr built-in functions.
In reality, it is fairly easy for a provider to plug-intogremlin-objects
simply by supplying aGraphTraversalSource
of their choosing.The ability to register custom primitive types and traversal result parsers allows for further customization.Sinceneo4j
already has it’s ownNeo4jGraph, it’s a good candidate to become the next test case.
Some providers useGraphFrames to execute bulk operations and graph algorithms on top of Tinkerpop.Assuming they can work withDataFrames, one could build aGraphTraversalSource
,which translates the objectGraph
andQuery
operations intoDataFrame
tables, and adapt’s it to the provider’sGraphFrame
.
TheAnyTraversal andSubTraversalinterfaces extendFormattable so that the steps defined in it’s body can be revealed.Let’s say that we stored the bytecode of these types of functional fields as a hidden property in the element.That could potentially allow us to executeuser defined traversals
using a, say,traversal.call('function-name')
step.
About
An Object Graph Mapping Library For Gremlin