The information here is deprecated.
In this guide you'll find examples of core operations provided by this plugin.
Check out check out the Format and neo-tools library for details on why and how we represent nested structures in the database and OpenAPI schema to get a general idea for the plugin.
You can follow examples using Django-provided shell of
complex_restproject.
We get vertices and edges in the form of Python dictionaries (check out the Format for details). Here is a couple of simple examples.
vertex = {
"primitiveID": "amy",
"properties": {
"height": {"value": 167},
"age": {"type": "days"}
}
}Front-end guys call these nodes, but we use the term vertex because Neo4j has its own nodes, and they are different.
edge = {
"sourceNode": "amy",
"sourcePort": "mobile",
"targetNode": "bob",
"targetPort": "laptop",
"connection": {
"status": 7,
"online": True
}
}Edges represent a connection between two nodes and their corresponding ports.
We get these in the form of graphs, like so:
graph = {
"nodes": [
{"primitiveID": "amy"},
{"primitiveID": "bob"}
],
"edges": [
{
"sourceNode": "amy",
"sourcePort": "mobile",
"targetNode": "bob",
"targetPort": "laptop"
}
]
}We run validation on this data before we proceed further: check consistent IDs, etc. You can see more in
serializersandfieldsmodules.
We want to represent these as graph structures inside Neo4j. Unfortunately, Neo4j cannot store nested objects on either nodes or relationships, so we do not have 1:1 mappings for vertices-nodes and edges-relationships. Instead, we represent vertices & edges as tree structures and convert back and forth between them. Check out the Format document for more information.
To support that, we use the Converter class:
>>> from converters import Converter
>>> converter = Converter()Converter can translate between the graph dictionary and structures which can be saved with Neo4j - back and forth:
>>> subgraph = converter.load(graph)
>>> original = converter.dump(subgraph)
>>> graph == original
TrueWe use py2neo library to work with Neo4j. subgraph is a Subgraph object - an arbitrary collection of nodes and relationships.
To start, we need a connection to Neo4j database. Neo4jGraphManager provides just that, as well as all database-related operations we are interested in.
>>> from managers import Neo4jGraphManager
>>> manager = Neo4jGraphManager("bolt://localhost:7687", auth=("neo4j", "neo4j"))
>>> manager.clear() # reset Neo4j databaseLet's create some fragments to work with. These behave just like Django models, except they are Neo4j OGM models from py2neo.
>>> from models import Fragment
>>> marketing = Fragment(name="marketing")
>>> research = Fragment(name="research")
>>> sales = Fragment(name="sales")For now these fragments are unbound: they exist only in memory, there is no counterpart in Neo4j database. We bind them by saving these with the help of a FragmentManager.
FragmentManagerprovides basic CRUD operations for fragments.
>>> # .fragments is a FragmentManager for this graph
>>> manager.fragments.save(marketing, research, sales)Now we have 3 fragments in the database. We use fragments to partition the graph into regions for security control and ease of work: each fragment may contain a set of vertices and edges between them.
The graph has a special root fragment, which includes all content of a graph (vertices, edges, groups, etc). All new and existing entities are included in the root fragment by default.
Additional notes:
- Currently, the security control is not implemented.
- Under the hood, we store fragments as nodes with a specific label.
- Containment is implemented as a relationship between the fragment node and root nodes of entities (vertices, edges, groups):
(fragment) --> (entity) - Currently, we have no idea how to handle connections & interaction between fragments.
- Currently, the root fragment is a logical construct; not an instance of
Fragmentmodel.
The main operation is a merge with replacement. We can do this either for a given fragment, or for the whole graph using the root fragment.
Let's add some graph data to the marketing fragment:
>>> data = {'edges': [], 'nodes': [{'primitiveID': 'amy'}, {'primitiveID': 'bob'}]}
>>> subgraph = converter.load(data)
>>> # .content is a ContentManger for this graph
>>> manager.fragments.content.replace(subgraph, marketing)We just merged (with replacement) new content into the fragment marketing. Now the fragment node has a link to the roots of 2 vertex trees: a tree-like subgraph of nodes and relationships representing our initial data. Each tree stores the data for a corresponding node.
Remember that we cannot save nested data structures as Neo4j properties. We represent these as tree-like entities on the backend.
We can get this data back just as easy:
>>> subgraph = manager.fragments.content.read(marketing)
>>> data = converter.dump(subgraph)
>>> data
{'edges': [], 'nodes': [{'primitiveID': 'amy'}, {'primitiveID': 'bob'}]}Now for something interesting - let's try and save the following graph in the same fragment:
>>> data = {'edges': [], 'nodes': [{'primitiveID': 'amy'}, {'primitiveID': 'dan'}]}
>>> subgraph = converter.load(data)
>>> manager.fragments.content.replace(subgraph, marketing)The idea here is that we want to replace the content of the marketing fragment in a smart way:
- create new nodes & relationships
- update existing stuff with new data if needed
- delete the old stuff
We also want to preserve existing relationships between updated nodes and other entities in the graph. Here's how we do it:
- Merge the root nodes of the following entity trees:
- Vertex trees.
- Edge trees.
- Group trees (if any).
- Remove old nodes & relationships.
- Re-link fragment with the root nodes of new entities to be created.
- Merge the rest and fragment-entity links.
See
managers.ContentManager._mergefor details.
In the example above, we create dan, delete bob and update amy vertices. We preserve all connections from amy vertex to other intact members of the same graph.
Now the fragment contains just two vertices:
>>> subgraph = manager.fragments.content.read(marketing)
>>> data = converter.dump(subgraph)
>>> data
{'edges': [], 'nodes': [{'primitiveID': 'amy'}, {'primitiveID': 'dan'}]}Let's add some more data to another fragment:
>>> data = {
... 'edges': [{'sourceNode': 'bob',
... 'sourcePort': 'mobile',
... 'targetNode': 'cloe',
... 'targetPort': 'laptop'}],
... 'nodes': [{'primitiveID': 'bob'}, {'primitiveID': 'cloe'}]}
>>> subgraph = converter.load(data)
>>> manager.fragments.content.replace(subgraph, research)The research fragment now contains 2 vertex trees and 1 edge tree, with a relationship between roots of entity trees:
(bob_root) --> (edge_root) --> (cloe_root)
We can get the whole graph (same as the root fragment):
>>> # reads root fragment by default
>>> subgraph = manager.fragments.content.read()
>>> data = converter.dump(subgraph)
>>> data
{'edges': [{'sourceNode': 'bob',
'sourcePort': 'mobile',
'targetNode': 'cloe',
'targetPort': 'laptop'}],
'nodes': [{'primitiveID': 'amy'},
{'primitiveID': 'bob'},
{'primitiveID': 'cloe'},
{'primitiveID': 'dan'}]}Now for something interesting. Let's save the following graph on the root fragment:
>>> data = {
... 'edges': [{'sourceNode': 'bob',
... 'sourcePort': 'IoT device',
... 'targetNode': 'eve',
... 'targetPort': 'server'}],
... 'nodes': [{'primitiveID': 'amy'},
... {'primitiveID': 'bob'},
... {'primitiveID': 'eve'}]}
>>> subgraph = converter.load(data)
>>> # replaces root fragment by default
>>> manager.fragments.content.replace(subgraph)Here we:
- create
evevertex andbob-eveedge - update
amyandbobvertices while preserving relationships to parent fragment - delete vertices
cloe,danandbob-cloeedge
The state of fragments' content:
marketingfragment still containsamyvertexresearchfragment still hasbobvertexevevertex andbob-eveedge do not belong to any fragment
Notes:
ContentMangeris responsible for all the logic related to graph updates.