Using the OGM

Goblin aims to provide a powerful Object Graph Mapper (OGM) while maintaining a simple, transparent interface. This document describes the OGM components in more detail.

Modeling Graph Elements with Goblin

At the core of the Goblin is the concept of the graph element. TinkerPop 3 (TP3) uses three basic kinds of elements: Vertex, Edge, and Property. In order to achieve consistent mapping between Python objects and TP3 elements, Goblin provides three corresponding Python base classes that are used to model graph data: Vertex, Edge, and Property. While these classes are created to interact smoothly with TP3, it is important to remember that Goblin does not attempt to implement the same element interface found in TP3. Indeed, other than user defined properties, Goblin elements feature little to no interface. To begin modeling data, simply create model element classes that inherit from the goblin.element classes. For example:

import goblin
from aiogremlin.gremlin_python import Cardinality


class Person(goblin.Vertex):
    pass


class City(goblin.Vertex):
    pass


class BornIn(goblin.Edge):
    pass

And that is it, these are valid element classes that can be saved to the graph database. Using these three classes we can model a series of people that are connected to the cities in which they were born. However, these elements aren’t very useful, as they don’t contain any information about the person or place. To remedy this, add some properties to the classes.

Using goblin.properties

Using the properties module is a bit more involved, but it is still pretty easy. It simply requires that you create properties that are defined as Python class attributes, and each property requires that you pass a DataType class or instance as the first positional argument. This data type, which is a concrete class that inherits from DataType, handles validation, as well as any necessary conversion when data is mapped between the database and the OGM. Goblin currently ships with 4 data types: String, Integer, Float, and Boolean. Example property definition:

import goblin


class Person(goblin.Vertex):
    name = goblin.Property(goblin.String)


class City(goblin.Vertex):
    name = goblin.Property(goblin.String)
    population = goblin.Property(goblin.Integer)


class BornIn(goblin.Edge):
    pass

Goblin properties can also be created with a default value, set by using the kwarg default in the class definition:

class BornIn(goblin.Edge):
    date = goblin.Property(goblin.String, default='unknown')

Creating Elements and Setting Property Values

Behind the scenes, a small metaclass (the only metaclass used in Goblin), substitutes a PropertyDescriptor for the Property, which provides a simple interface for defining and updating properties using Python’s descriptor protocol:

>>> leif = Person()
>>> leif.name = 'Leif'

>>> detroit = City()
>>> detroit.name = 'Detroit'
>>> detroit.population =    5311449  # CSA population

# change a property value
>>> leif.name = 'Leifur'

In the case that an invalid property value is set, the validator will raise a ValidationError immediately:

>>> detroit.population = 'a lot of people'
ValidationError: Not a valid integer: a lot of people

Creating Edges

Creating edges is very similar to creating vertices, except that edges require that a source (outV) and target (inV) vertex be specified. Both source and target nodes must be Goblin vertices. Furthermore, they must be created in the database before the edge. This is further discussed below in the Session section. Source and target vertices may be passed to the edge on instantiation, or added using the property interface:

>>> leif_born_in_detroit = BornIn(leif, detroit)
# or
>>> leif_born_in_detroit = BornIn()
>>> leif_born_in_detroit.source = leif
>>> leif_born_in_detroit.target = detroit
>>> leif_born_in_detroit.date  # default value
'unknown'

Vertex Properties

In addition to the aforementioned elements, TP3 graphs also use a special kind of property, called a vertex property, that allows for list/set cardinality and meta-properties. To accommodate this, Goblin provides a class VertexProperty that can be used directly to create multi-cardinality properties:

class Person(goblin.Vertex):
    name = goblin.Property(goblin.String)
    nicknames = goblin.VertexProperty(
        goblin.String, card=Cardinality.list)


>>> david = Person()
>>> david.name = 'David'
>>> david.nicknames = ['Dave', 'davebshow']

Notice that the cardinality of the VertexProperty must be explicitly set using the card kwarg and the Cardinality enumerator.

VertexProperty provides a different interface than the simple, key/value style PropertyDescriptor in order to accomodate more advanced functionality. For accessing multi-cardinality vertex properties, Goblin provides several helper classes called managers. The managers inherits from list or set (depending on the specified cardinality), and provide a simple API for accessing and appending vertex properties. To continue with the previous example, we see the dave element’s nicknames:

>>> david.nicknames
[<VertexProperty(type=<goblin.properties.String object at 0x7f87a67a3048>, value=Dave),
 <VertexProperty(type=<goblin.properties.String object at 0x7f87a67a3048>, value=davebshow)]

To add a nickname without replacing the earlier values, we simple append() as if the manager were a Python list:

>>> david.nicknames.append('db')
>>> david.nicknames
[<VertexProperty(type=<goblin.properties.String object at 0x7f87a67a3048>, value=Dave),
 <VertexProperty(type=<goblin.properties.String object at 0x7f87a67a3048>, value=davebshow),
 <VertexProperty(type=<goblin.properties.String object at 0x7f87a67a3048>, value=db)]

If this were a VertexProperty with a set cardinality, we would simply use add() to achieve similar functionality.

Both ListVertexPropertyManager and SetVertexPropertyManager provide a simple way to access a specific VertexProperty. You simply call the manager, passing the value of the vertex property to be accessed:

>>> db = dave.nicknames('davebshow')
<VertexProperty(type=<goblin.properties.String object at 0x7f87a67a3048>, value=davebshow)

The value of the vertex property can be accessed using the value property:

>>> db.value
'davebshow'

Meta-properties

VertexProperty can also be used as a base classes for user defined vertex properties that contain meta-properties. To create meta-properties, define a custom vertex property class just like you would any other element, adding as many simple (non-vertex) properties as needed:

class HistoricalName(goblin.VertexProperty):
    notes = goblin.Property(goblin.String)

Now, the custom VertexProperty can be added to a vertex class, using any cardinality:

class City(goblin.Vertex):
    name = goblin.Property(goblin.String)
    population = goblin.Property(goblin.Integer)
    historical_name = HistoricalName(
        goblin.String, card=Cardinality.list)

Now, meta-properties can be set on the VertexProperty using the descriptor protocol:

>>> montreal = City()
>>> montreal.historical_name = ['Ville-Marie']
>>> montreal.historical_name('Ville-Marie').notes = 'Changed in 1705'

And that’s it.

Saving Elements to the Database Using Session

All interaction with the database is achieved using the Session object. A Goblin session should not be confused with a Gremlin Server session, although in future releases it will provide support for server sessions and transactions. Instead, the Session object is used to save elements and spawn Gremlin traversals. Furthemore, any element created using a session is live in the sense that a Session object maintains a reference to session elements, and if a traversal executed using a session returns different property values for a session element, these values are automatically updated on the session element. Note - the examples shown in this section must be wrapped in coroutines and ran using the asyncio.BaseEventLoop, but, for convenience, they are shown as if they were run in a Python interpreter. To use a Session, first create a Goblin App using Goblin.open, then register the defined element classes:

>>> app = await goblin.Goblin.open(loop)
>>> app.register(Person, City, BornIn)
>>> session = await app.session()

Goblin application support a variety of configuration options, for more information see the Goblin application documentation.

The best way to create elements is by adding them to the session, and then flushing the pending queue, thereby creating the elements in the database. The order in which elements are added is important, as elements will be created based on the order in which they are added. Therefore, when creating edges, it is important to add the source and target nodes before the edge (if they don’t already exits). Using the previously created elements:

>>> session.add(leif, detroit, leif_born_in_detroit)
>>> await session.flush()

And that is it. To see that these elements have actually been created in the db, check that they now have unique ids assigned to them:

>>> assert leif.id
>>> assert detroit.id
>>> assert leif_born_in_detroit.id

For more information on the Goblin App, please see Using the Goblin App

Session provides a variety of other CRUD functions, but all creation and updating can be achieved simply using the add() and flush() methods.

Writing Custom Gremlin Traversals

Finally, Session objects allow you to write custom Gremlin traversals using the official gremlin-python Gremlin Language Variant (GLV). There are two methods available for writing session based traversals. The first, traversal, accepts an element class as a positional argument. This is merely for convenience, and generates this equivalent Gremlin:

>>> session.traversal(Person)
g.V().hasLabel('person')

Or, simply use the property g:

>>> session.g.V().hasLabel('person')...

In general property names are mapped directly from the OGM to the database. However, by passing the db_name kwarg to a property definition, the user has the ability to override this behavior. To avoid mistakes in the case of custom database property names, it is encouraged to access the mapped property names as class attributes:

>>> Person.name
'name'

So, to write a traversal:

>>> session.traversal(Person).has(Person.name, 'Leifur')

Also, it is important to note that certain data types could be transformed before they are written to the database. Therefore, the data type method to_db may be required:

>>> session.traversal(Person).has(
...     Person.name, goblin.String.to_db('Leifur'))

While this is not the case with any of the simple data types shipped with Goblin, custom data types or future additions may require this kind of operation. Because of this, Goblin includes the convenience function bindprop, which also allows an optional binding for the value to be specified:

>>> traversal = session.traversal(Person)
>>> traversal.has(bindprop(Person, 'name', 'Leifur', binding='v1'))

Finally, there are a variety of ways to to submit a traversal to the server. First of all, all traversals are themselve asynchronous iterators, and using them as such will cause a traversal to be sent on the wire:

>>> async for msg in session.g.V().hasLabel('person'):
...     print(msg)

Furthermore, Goblin provides several convenience methods that submit a traversal as well as process the results toList(), toSet() and next(). These methods both submit a script to the server and iterate over the results. Remember to await the traversal when calling these methods:

>>> traversal = session.traversal(Person)
>>> leif = await traversal.has(
...     bindprop(Person, 'name', 'Leifur', binding='v1')).next()

And that is pretty much it. We hope you enjoy the Goblin OGM.