Vor kurzem haben wir einige Tests mit der aktuellen Structr-Version durchgeführt, um zu sehen, wie gut Structr mit tief verschachtelten JSON-Dokumenten umgehen kann, um Graphenstrukturen in Neo4j zu erstellen. Wir haben herausgefunden, dass es bis zur zweiten Ebene der Objektverschachtelung recht gut funktioniert, und zwar nur dann, wenn neue Objekte entweder über ihre UUID oder über einen skalaren Wert auf bestehende Objekte verweisen, wenn die entsprechende Zuordnung im Schema definiert wurde.
Wenn es um Attribute von verschachtelten Objekten geht, die von komplexeren Werten referenziert werden, wie Sammlungen oder verschachtelten Objekten selbst, wurde es insofern schwierig, als dass man ein ziemlich komplexes Objekt-Mapping im Schema definieren musste, indem man Dinge wie Notion-Attribute verwendete (unter Verwendung von PropertySetNotion intern).
Was gibt es Neues?
In den letzten Tagen hat Christian einige Verbesserungen vorgenommen, um diese Probleme zu entschärfen und die Erstellung von Knoten und Beziehungen in Neo4j basierend auf den Schemaregeln in Structr zu vereinfachen.
Beispiel
Um die neuen Möglichkeiten zu demonstrieren, haben wir das folgende Beispiel erstellt.
Schema

Die Schemaregeln in Cypher-Notation:
(:Project)-[:TASK]->(:Task)
(:Task)-[:SUBTASK]->(:Task)
(:Task)<-[WORKS_ON]-(:Worker)
(:Worker)->[:WORKS_AT]->(:Company)
Die Kardinalitäten sind:
- Projekt 1 -> * Aufgabe
- Aufgabe 1 -> * Aufgabe
- Arbeiter 1 -> * Aufgabe
- Arbeiter * -> 1 Unternehmen
Damit das Beispiel funktioniert, ist es wichtig, die automatische Benennung zu überschreiben und die Attribute genau wie im Beispiel zu benennen: tasks, parentTask, subtasks, company, worker, workers usw..
Stellen Sie sicher, dass das Attribut name für jeden Typ eindeutig ist.
Regeln für die automatische Erstellung
Anstatt eine PropertyNotion mit autocreate müssen Sie nur die folgenden Regeln für die automatische Erstellung (ALWAYS) im Schema-Editor definieren:
- Projekt -> Aufgabe
- Aufgabe -> Aufgabe
- Arbeiter -> Aufgabe
- Arbeiter -> Unternehmen
JSON-Dokument
Das folgende Beispiel-JSON-Dokument enthält alle Informationen über die zu erstellenden Objekte. Beachten Sie, dass einige Unterobjekte mehrfach im Dokument vorkommen, und wenn für ihren Typ ein eindeutiges Attribut definiert ist (wie z. B. das Attribut name für Projekt, Unternehmen und Arbeitnehmer), wird das Objekt nur einmal erstellt.
{
"name": "Project1",
"tasks": [
{
"name": "Task1",
"worker": {
"name": "Worker1",
"company": {
"name": "Company1"
}
},
"subtasks": [
{
"name": "Subtask1.1",
"worker": {
"name": "Worker1",
"company": {
"name": "Company1"
}
}
},
{
"name": "Subtask1.2",
"worker": {
"name": "Worker2",
"company": {
"name": "Company1"
}
}
},
{
"name": "Subtask1.3",
"worker": {
"name": "Worker2",
"company": {
"name": "Company1"
}
}
},
{
"name": "Subtask1.4",
"worker": {
"name": "Worker3",
"company": {
"name": "Company2"
}
}
}
]
},
{
"name": "Task2",
"worker": {
"name": "Worker2",
"company": {
"name": "Company1"
}
}
},
{
"name": "Task3",
"worker": {
"name": "Worker3",
"company": {
"name": "Company2"
}
}
},
{
"name": "Task4",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
},
"subtasks": [
{
"name": "Subtask4.1",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
}
},
{
"name": "Subtask4.2",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
}
},
{
"name": "Subtask4.3",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
}
},
{
"name": "Subtask4.4",
"worker": {
"name": "Worker5",
"company": {
"name": "Company3"
}
}
}
]
},
{
"name": "Task5",
"worker": {
"name": "Worker5",
"company": {
"name": "Company3"
}
},
"subtasks": [
{
"name": "Subtask5.1",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
},
"subtasks": [
{
"name": "Subtask5.1.1",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
}
},
{
"name": "Subtask5.1.2",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
}
}
]
},
{
"name": "Subtask5.2",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
},
"subtasks": [
{
"name": "Subtask5.2.1",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
}
},
{
"name": "Subtask5.2.2",
"worker": {
"name": "Worker4",
"company": {
"name": "Company3"
}
}
}
]
}
]
}
]
}
Speichern Sie dieses Dokument einfach in einer Datei und POSTen Sie es an den REST-Endpunkt /projects:
$ curl -i -HX-User:admin -HX-Password:admin "http://0.0.0.0:8082/structr/rest/projects" -XPOST -d @/tmp/project.json
HTTP/1.1 100 Continue
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Set-Cookie: JSESSIONID=rxjhtzyxetnj1l8dx6vg8aejq;Path=/
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Location: http://0.0.0.0:8082/structr/rest/projects/fafeada746ef432196ee2ccfc7e362fc
Vary: Accept-Encoding, User-Agent
Content-Length: 121
Server: Jetty(9.1.4.v20140401)
{
"result_count": 1,
"result": [
"fafeada746ef432196ee2ccfc7e362fc"
],
"serialization_time": "0.000226953"
}
Es wurde ein komplettes Diagramm erstellt, ohne dass eine Redundanz entstand!
curl -i -HX-User:admin -HX-Password:admin "http://0.0.0.0:8082/structr/rest/projects/fafeada746ef432196ee2ccfc7e362fc/ui"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Set-Cookie: JSESSIONID=el6ri37v61wzeuoni7ilgrl0;Path=/
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Vary: Accept-Encoding, User-Agent
Content-Length: 1222
Server: Jetty(9.1.4.v20140401)
{
"query_time": "0.001580893",
"result_count": 1,
"result": {
"id": "fafeada746ef432196ee2ccfc7e362fc",
"name": "Project1",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Project",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:13+0100",
"lastModifiedDate": "2014-12-15T17:47:13+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"tasks": [
{
"id": "517c1a89e44f479eb0802b9045271b4c",
"name": "Task1"
},
{
"id": "dace6757bad94aa0a137420741406699",
"name": "Task2"
},
{
"id": "097729f47768469ebeaacd00ea8a442e",
"name": "Task3"
},
{
"id": "27bfdc1bb293458eab0d912811f610da",
"name": "Task4"
},
{
"id": "b762fd8f2fe24d149d4a220412c56f49",
"name": "Task5"
}
]
},
"serialization_time": "0.000166250"
}
curl -i -HX-User:admin -HX-Password:admin "http://0.0.0.0:8082/structr/rest/companies/ui"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Set-Cookie: JSESSIONID=1mc9hmhm2umrm1h60h1vt56o7c;Path=/
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Vary: Accept-Encoding, User-Agent
Content-Length: 2668
Server: Jetty(9.1.4.v20140401)
{
"query_time": "0.002427711",
"result_count": 3,
"result": [
{
"id": "bc9de3a97bce409da5234e5976355aa9",
"name": "Company1",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Company",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:12+0100",
"lastModifiedDate": "2014-12-15T17:47:12+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"workers": [
{
"id": "2da31b7175cb44c787ff93fe43c7e317",
"name": "Worker1"
},
{
"id": "d980f56aacea4a0bb2cb8cf7b3a740d5",
"name": "Worker2"
}
]
},
{
"id": "8fac823ef76f436c96cfc9e0c4c21fb5",
"name": "Company2",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Company",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:12+0100",
"lastModifiedDate": "2014-12-15T17:47:12+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"workers": [
{
"id": "a9a23f57b9ce4e33bcc1efbfd2537164",
"name": "Worker3"
}
]
},
{
"id": "eb34f6449d69484f93c69320fe95ea24",
"name": "Company3",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Company",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:13+0100",
"lastModifiedDate": "2014-12-15T17:47:13+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"workers": [
{
"id": "eadcb538f90a41838f0196fd74b82037",
"name": "Worker4"
},
{
"id": "8f8bd3613aa543ecae222da22cdd2e14",
"name": "Worker5"
}
]
}
],
"serialization_time": "0.000221961"
}
curl -i -HX-User:admin -HX-Password:admin "http://0.0.0.0:8082/structr/rest/workers/ui"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Set-Cookie: JSESSIONID=1b6g61a7uo1t016croq1fz2x41;Path=/
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Vary: Accept-Encoding, User-Agent
Content-Length: 6253
Server: Jetty(9.1.4.v20140401)
{
"query_time": "0.002056031",
"result_count": 5,
"result": [
{
"id": "2da31b7175cb44c787ff93fe43c7e317",
"name": "Worker1",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Worker",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:12+0100",
"lastModifiedDate": "2014-12-15T17:47:12+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"company": {
"id": "bc9de3a97bce409da5234e5976355aa9",
"name": "Company1"
},
"tasks": [
{
"id": "9988348105e34b1ab5d365f4e4f7262a",
"name": "Subtask1.1"
},
{
"id": "517c1a89e44f479eb0802b9045271b4c",
"name": "Task1"
}
]
},
{
"id": "d980f56aacea4a0bb2cb8cf7b3a740d5",
"name": "Worker2",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Worker",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:12+0100",
"lastModifiedDate": "2014-12-15T17:47:12+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"company": {
"id": "bc9de3a97bce409da5234e5976355aa9",
"name": "Company1"
},
"tasks": [
{
"id": "196189bde51a43afb587563fb47fda91",
"name": "Subtask1.2"
},
{
"id": "bd35947338924daa85b13391083551b1",
"name": "Subtask1.3"
},
{
"id": "dace6757bad94aa0a137420741406699",
"name": "Task2"
}
]
},
{
"id": "a9a23f57b9ce4e33bcc1efbfd2537164",
"name": "Worker3",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Worker",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:12+0100",
"lastModifiedDate": "2014-12-15T17:47:12+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"company": {
"id": "8fac823ef76f436c96cfc9e0c4c21fb5",
"name": "Company2"
},
"tasks": [
{
"id": "779b76705c9b43d598ce971024743b13",
"name": "Subtask1.4"
},
{
"id": "097729f47768469ebeaacd00ea8a442e",
"name": "Task3"
}
]
},
{
"id": "eadcb538f90a41838f0196fd74b82037",
"name": "Worker4",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Worker",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:13+0100",
"lastModifiedDate": "2014-12-15T17:47:13+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"company": {
"id": "eb34f6449d69484f93c69320fe95ea24",
"name": "Company3"
},
"tasks": [
{
"id": "8b2dbfcc93b94e428052ff1276991e34",
"name": "Subtask4.1"
},
{
"id": "f4b99c128cc24c518e6f9af5c3affea4",
"name": "Subtask4.2"
},
{
"id": "e6156b8b566e45349e671229ff70f9ea",
"name": "Subtask4.3"
},
{
"id": "27bfdc1bb293458eab0d912811f610da",
"name": "Task4"
},
{
"id": "5e45f0d22ee944ec84bc31aa75b40dda",
"name": "Subtask5.1.1"
},
{
"id": "2e21cad09d09423dacec07abcc763c3f",
"name": "Subtask5.1.2"
},
{
"id": "ad2e56c80ea944808b7082cdcab9f659",
"name": "Subtask5.1"
},
{
"id": "6173bc32066b40498147630d92c17990",
"name": "Subtask5.2.1"
},
{
"id": "83889c6a55f043c6bd69dc04614ff76f",
"name": "Subtask5.2.2"
},
{
"id": "e04e9f77fa444c40904b474f27bcdc61",
"name": "Subtask5.2"
}
]
},
{
"id": "8f8bd3613aa543ecae222da22cdd2e14",
"name": "Worker5",
"owner": {
"id": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"name": "admin"
},
"type": "Worker",
"createdBy": "f02e59a47dc9492da3e6cb7fb6b3ac25",
"deleted": false,
"hidden": false,
"createdDate": "2014-12-15T17:47:13+0100",
"lastModifiedDate": "2014-12-15T17:47:13+0100",
"visibleToPublicUsers": false,
"visibleToAuthenticatedUsers": false,
"visibilityStartDate": null,
"visibilityEndDate": null,
"company": {
"id": "eb34f6449d69484f93c69320fe95ea24",
"name": "Company3"
},
"tasks": [
{
"id": "7785c5445d2147578bc8831797241e53",
"name": "Subtask4.4"
},
{
"id": "b762fd8f2fe24d149d4a220412c56f49",
"name": "Task5"
}
]
}
],
"serialization_time": "0.000484448"
}
Ist das nicht faszinierend? 🙂
Wie es funktioniert
Arbeitsablauf
Der Parser (GSON) erzeugt aus dem JSON eine verschachtelte Struktur von Maps (JsonInput), die rekursiv mit den Schemaregeln abgeglichen werden, beginnend mit dem innersten Objekt. Structr verwendet eine so genannte „DeserializationStrategy“, um herauszufinden, ob ein verschachteltes Objekt bereits im Graphen existiert (und daher direkt verlinkt werden kann), oder ob es gemäß den Autocreation-Regeln im Schema erstellt werden sollte.
Rekursive Auswertung
Beim Parsen des obigen JSON-Dokuments sucht Structr nach einem Projekt mit dem Namen „Project1“ und einem Array von Aufgaben mit den Namen „Task1“ bis „Task5“. Um den ersten Task mit dem Namen `Task1` zu finden, ruft Structr rekursiv die DeserializationStrategy auf, um den gewünschten Task zu erhalten, und wiederholt dies für den Worker und das Unternehmen des Workers. Da die Entität „Firma“ keine weiteren verschachtelten Elemente hat, wird die Rekursion beendet und Structr sucht in der Datenbank nach einer Firma mit dem Namen „Firma1“. Da die Firma nicht existiert, wird sie aufgrund der Einstellungen für die automatische Erstellung angelegt und zur vorherigen Rekursionsebene zurückgeführt, wo sie mit dem neu erstellten Arbeiter mit dem Namen „Arbeiter1“ verknüpft wird.
Dieser Prozess erstellt rekursiv neue Entitäten oder holt sie aus der Datenbank, wenn sie bereits existieren, und bildet das verschachtelte JSON-Dokument gemäß den Schemaregeln auf eine Graphstruktur ab.
Structr als Dokumentendatenbank
Das beschriebene Feature wird die Dokumentendatenbank-Fähigkeiten von Structr erheblich erweitern und Teil der kommenden Version 1.1 sein.
Den Testcode für dieses spezielle Beispiel finden Sie unter dem folgenden Link:
https://github.com/structr/structr/blob/master/structr-rest/src/test/java/org/structr/rest/document/DocumentTest.java
</Verkürzung der Einsatzzeiten um bis zu 95 %;p>