(Edit Jan 24, 2015: added Transaction handling and side note (this is the last update here, all further tutorial versions you'll find here.)
The other day I've played a bit with the Open Source Graph database Neo4j (for no reasons, just to learn something new), and the usual "what if..." came to my mind and I've started to code.
The result is a Neo4j ABAP Connector called Neo4a, available under Apache License 2.0 on Github: Neo4a
The whole project is coded under Netweaver ABAP Stack 7.40 SP8. It will not work on lower releases and will not be downported (sorry).
Installing and running Neo4j (example for Linux)
In my case I've created a new virtual Linux machine (CentOS) under VMware to separate the database from my SAP server, but it should work also, if you are installing Neo4j on the same server (don't do this in production).
Just download the tar (choose your OS: http://neo4j.com/download/other-releases/), extract into a folder of your choice, call './bin/neo4j start' and you are done.
Installation of Neo4a
You need the most current ABAP JSON Document class (zJSON Version 2.28, available on Github https://github.com/se38/zJSON ).
Install the zJSON and Neo4a nuggets via SAPlink and activate the sources. It may be a good idea to move the classes into a new development package, but this is not required.
A small Tutorial for Neo4a
The following examples are taken from the Neo4j tutorial and are "translated" into ABAP.
(side note: if you screwed up and want to start all over again, just stop Neo4j with './bin/neo4j stop' and delete the content of the 'data' folder with 'rm -rf data/*'. An empty database will be recreated automatically if you start the database again)
Create nodes
"*--- points to the Neo4j DB host ---*
DATA(neo4a) = NEW zcl_neo4a('192.168.38.52').
TYPES: BEGINOF ty_actor,
name TYPE string,
ENDOF ty_actor,
BEGINOF ty_movie,
titleTYPE string,
ENDOF ty_movie.
DATA(actor) = VALUE ty_actor( name = 'Tom Hanks').
DATA(movie) = VALUE ty_movie(title = 'Sleepless IN Seattle').
TRY.
DATA(node_actor) = neo4a->create_node(
i_properties = actor
i_label = 'Actor'
).
DATA(node_movie) = neo4a->create_node(
i_properties = movie
i_label = 'Movie'
).
CATCH zcx_neo4a INTODATA(n4a_ex).
WRITE:/ n4a_ex->get_text().
RETURN.
ENDTRY.
Result: Two nodes
Create a relationship between the two nodes
DATA(neo4a) = NEW zcl_neo4a('192.168.38.52').
TRY.
DATA(node_actor) = neo4a->get_node(1).
DATA(node_movie) = neo4a->get_node(2).
DATA(relationship) = node_actor->create_relationship(
i_type = 'acted_in'
i_to_node = node_movie
).
CATCH zcx_neo4a INTODATA(n4a_ex).
WRITE:/ n4a_ex->get_text().
RETURN.
ENDTRY.
Result: the two nodes are conected
Set/Get properties
DATA(neo4a) = NEW zcl_neo4a('192.168.38.52').
TRY.
DATA(node) = neo4a->get_node(1).
node->set_property(
EXPORTING
i_name = 'year_of_birth'
i_value = '1944'
).
DATA(name) = node->get_property('name').
DATA(yob) = node->get_property('year_of_birth').
cl_demo_output=>display( |{ name } : { yob }| ).
CATCH zcx_neo4a INTODATA(n4a_ex).
WRITE:/ n4a_ex->get_text().
RETURN.
ENDTRY.
Get all properties
You can get the properties of a node as data object, JSON string of Name/Value pairs.
DATA(neo4a) = NEW zcl_neo4a('192.168.38.52').
DATA: BEGINOF actor,
name TYPE string,
year_of_birth TYPE string,
ENDOF actor.
TRY.
neo4a->get_node(1)->get_properties(
IMPORTING
e_properties = actor
e_properties_json = DATA(properties_json)
e_properties_table = DATA(properties_table)
).
cl_demo_output=>begin_section('DATA').
cl_demo_output=>write( actor ).
cl_demo_output=>begin_section('JSON').
cl_demo_output=>write( properties_json ).
cl_demo_output=>begin_section('Name/Value pairs').
cl_demo_output=>write( properties_table ).
cl_demo_output=>display().
CATCH zcx_neo4a INTODATA(n4a_ex).
WRITE:/ n4a_ex->get_text().
RETURN.
ENDTRY.
Result:
Get all nodes with a label
DATA(neo4a) = NEW zcl_neo4a('192.168.38.52').
TYPES: BEGINOF ty_movie,
titleTYPE string,
ENDOF ty_movie.
DATA(movie) = VALUE ty_movie(title = 'Forrest Gump').
TRY.
"*--- create another movie ---*
DATA(node_movie) = neo4a->create_node(
i_properties = movie
i_label = 'Movie'
).
"*--- and connect to Tom ---*
neo4a->get_node(1)->create_relationship(
EXPORTING
i_type = 'acted_in'
i_to_node = node_movie
).
"*--- get all movies ---*
neo4a->get_nodes_with_label(
EXPORTING
i_label = 'Movie'
IMPORTING
e_nodes = DATA(movies)
).
LOOPAT movies ASSIGNING FIELD-SYMBOL(<movie>).
cl_demo_output=>write_text(<movie>->get_property('title')).
ENDLOOP.
cl_demo_output=>display().
CATCH zcx_neo4a INTODATA(n4a_ex).
WRITE:/ n4a_ex->get_text().
RETURN.
ENDTRY.
The second movie is created and connected to Tom:
The list contains all movies:
Queries
To submit queries to the database we are using the Neo4j Cypher Query Language
DATA(neo4a) = NEW zcl_neo4a('192.168.38.52').
"*--- get all movies where Tom acted in ---*
DATA(query) = `MATCH (actor:Actor {name:'Tom Hanks'})-[r:acted_in]->(movie:Movie) RETURN movie.title`.
TRY.
neo4a->query(
EXPORTING
i_cypher = query
IMPORTING
e_result = DATA(result)
).
cl_demo_output=>display_json( result ).
CATCH zcx_neo4a INTODATA(n4a_ex).
WRITE:/ n4a_ex->get_text().
RETURN.
ENDTRY.
Result:
Transactions
(time to remember the side note at the top of this tutorial)
With transactions you can make a number of requests, each of which executes additional statements, and keeps the transaction open by resetting the transaction timeout. After the timeout an automatically rollback occurres. You can see the timeout in every response of a request to a new or open transaction.
"transaction" : {
"expires" : "Fri, 24 Jan 2015 22:53:51 +0000"
},
Now let's have a look at the following example:
DATA(neo4a) = NEW zcl_neo4a('192.168.38.52').
DATA(statements) = VALUE string_table(
(
`CREATE (matrix1:Movie { title : 'The Matrix', year : '1999-03-31' })`&&
`CREATE (matrix2:Movie { title : 'The Matrix Reloaded', year : '2003-05-07' })`&&
`CREATE (matrix3:Movie { title : 'The Matrix Revolutions', year : '2003-10-27' })`&&
`CREATE (keanu:Actor { name:'Keanu Reeves' })` &&
`CREATE (laurence:Actor { name:'Laurence Fishburne' })`&&
`CREATE (carrieanne:Actor { name:'Carrie-Anne Moss' })`&&
`CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix1)`&&
`CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix2)`&&
`CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix3)`&&
`CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix1)`&&
`CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix2)`&&
`CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix3)`&&
`CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix1)`&&
`CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix2)`&&
`CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix3)`
)
(
`CREATE (whatever:Movie { title : 'Just another Movie', year : '2015-01-24' })`
)
).
DATA(next_statements) = VALUE string_table(
(
`CREATE (whatever2:Movie { title : 'Yet another Movie', year : '2015-01-24' })`
)
).
TRY.
"*--- create the transaction ---*
DATA(transaction) = neo4a->create_transaction().
"*--- send the DB statements ---*
transaction->send( i_statements = statements ).
transaction->send( i_statements = next_statements ).
"*--- placeholders without properties should be visible now ---*
cl_demo_output=>display('have a look in the Neo4j browser').
"*--- and rollback the work ---*
transaction->rollback().
"*--- look Mom, they are gone ---*
cl_demo_output=>display('and look again').
CATCH zcx_neo4a INTODATA(n4a_ex).
WRITE:/ n4a_ex->get_text().
RETURN.
ENDTRY.
After the first stop, you can see the placeholders in the Neo4j browser
Close the output window. After the next stop look again -> the placeholders are gone.
Now replace the last two statements with:
"*--- commit the work ---*
transaction->commit().
"*--- tahtah, the Matrix ---*
cl_demo_output=>display('the red pill please').
and try again -> after the last stop the Matrix is alive...
Image by W. Carter (Wikimedia)
More methods are already implemented (ie. deletes, just have a look at the class interfaces), others are in the pipeline (see the open issues on Github)