I use Spring Boot 1.5.3 and OGM of 2.1.2. The SDN version is 4.2.3 and I use neo4j 3.2.1 database. I modified the config file to fix the minor issue with chyper version so i Use chyper 3.1 as default langauge.
In this use case I have only one simple domain class called category. Every category can have one parent and multiple children like a classical tree structure.
Due to the high number of categories I prefer better performance over disk space needed for the database. Here is my domain class:
@NodeEntity
public class Category {
@GraphId
Long id;
@Convert(UuidStringConverter.class)
@Index(unique = true, primary = true)
UUID uuid;
@DateString("yy-MM-dd")
private Date dateAdded;
@Index(unique = true, primary = false)
private String name;
@Relationship(type = "parent", direction = Relationship.OUTGOING)
private Category parent;
@Relationship(type = "children", direction = Relationship.OUTGOING)
private Set<Category> children;
public Category() {
dateAdded = new Date();
uuid = UUID.randomUUID();
}
public Category(String name) {
this();
this.name = name;
}
public Category(String name, Category parent) {
this(name);
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Category getParent() {
return parent;
}
public void setParent(Category parent) {
this.parent = parent;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
/**
* @return the dateAdded
*/
public Date getDateAdded() {
return dateAdded;
}
/**
* @param dateAdded
* the dateAdded to set
*/
public void setDateAdded(Date dateAdded) {
this.dateAdded = dateAdded;
}
/**
* @return the children
*/
public Set<Category> getChildren() {
return children;
}
/**
* @param children
* the children to set
*/
public void setChildren(Set<Category> children) {
this.children = children;
}
public void addChildren(Category c) {
if (children == null) {
children = new HashSet<>();
}
children.add(c);
}
public void removeChildren(Category c) {
if (children != null) {
children.remove(c);
if (children.size() == 0) {
children = null;
}
};
}
public String toString(){
return name;
}
}
In practice it would not be necessary to hold the reference to parent and children of the category as neo4j "direction agnostic" but it is due to performance related issues.
The very strange behavior of SDN is that if I create a category (Non-fiction) and a subcategory (Math) everything works fine. If I create a new subcategory (Biology) under the same main category, there will be an unexpected relation between the previous subcategory and a main category (Math and Non-fiction). Log shows the following:
Create Non-fiction main category
2017-06-14 09:47:06.312 DEBUG 6788 --- [nio-8083-exec-4] o.s.b.w.f.OrderedRequestContextFilter : Bound request context to thread: org.apache.catalina.connector.RequestFacade@241797fe
2017-06-14 09:47:06.331 INFO 6788 --- [nio-8083-exec-4] o.n.o.drivers.http.request.HttpRequest : Thread: 21, url: http://neo4j:password@localhost:7474/db/data/transaction/commit, request: {"statements":[{"statement":"MATCH (n:`Category`) WHERE n.`name` = { `name_0` } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p, ID(n)","parameters":{"name_0":"Non-fiction"},"resultDataContents":["graph","row"],"includeStats":false}]}
2017-06-14 09:47:06.390 INFO 6788 --- [nio-8083-exec-4] o.n.o.drivers.http.request.HttpRequest : Thread: 21, url: http://localhost:7474/db/data/transaction/23, request: {"statements":[{"statement":"MATCH (n) WHERE n.uuid = { id } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p","parameters":{"id":"3a497cec-d67c-4c44-ab86-e6639dc60d13"},"resultDataContents":["graph"],"includeStats":false}]}
2017-06-14 09:47:06.465 INFO 6788 --- [nio-8083-exec-4] o.n.o.drivers.http.request.HttpRequest : Thread: 21, url: http://localhost:7474/db/data/transaction/24, request: {"statements":[{"statement":"UNWIND {rows} as row MERGE (n:`Category`{uuid: row.props.uuid}) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, row.type as type","parameters":{"rows":[{"nodeRef":-684249223,"type":"node","props":{"name":"Non-fiction","uuid":"3a497cec-d67c-4c44-ab86-e6639dc60d13","dateAdded":"17-06-14"}}]},"resultDataContents":["row"],"includeStats":false}]}
2017-06-14 09:47:06.545 INFO 6788 --- [nio-8083-exec-4] h.b.services.CategoryServiceImpl : Non-fiction category were created
2017-06-14 09:47:06.547 INFO 6788 --- [nio-8083-exec-4] o.n.o.drivers.http.request.HttpRequest : Thread: 21, url: http://localhost:7474/db/data/transaction/25, request: {"statements":[{"statement":"MATCH (n:`Category`) WITH n MATCH p=(n)-[*0..1]-(m) RETURN p","parameters":{},"resultDataContents":["graph"],"includeStats":false}]}
2017-06-14 09:47:06.571 DEBUG 6788 --- [nio-8083-exec-4] o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@241797fe
Create Math subcategory of Non-fiction parent category
2017-06-14 09:48:26.865 DEBUG 6788 --- [nio-8083-exec-7] o.s.b.w.f.OrderedRequestContextFilter : Bound request context to thread: org.apache.catalina.connector.RequestFacade@241797fe
2017-06-14 09:48:26.881 INFO 6788 --- [nio-8083-exec-7] o.n.o.drivers.http.request.HttpRequest : Thread: 24, url: http://neo4j:password@localhost:7474/db/data/transaction/commit, request: {"statements":[{"statement":"MATCH (n:`Category`) WHERE n.`name` = { `name_0` } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p, ID(n)","parameters":{"name_0":"Math"},"resultDataContents":["graph","row"],"includeStats":false}]}
2017-06-14 09:48:26.916 INFO 6788 --- [nio-8083-exec-7] o.n.o.drivers.http.request.HttpRequest : Thread: 24, url: http://localhost:7474/db/data/transaction/30, request: {"statements":[{"statement":"MATCH (n) WHERE n.uuid = { id } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p","parameters":{"id":"3a839cc1-9af7-45fd-a29c-bfe96705655e"},"resultDataContents":["graph"],"includeStats":false}]}
2017-06-14 09:48:26.939 INFO 6788 --- [nio-8083-exec-7] o.n.o.drivers.http.request.HttpRequest : Thread: 24, url: http://localhost:7474/db/data/transaction/31, request: {"statements":[{"statement":"UNWIND {rows} as row MERGE (n:`Category`{uuid: row.props.uuid}) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, row.type as type","parameters":{"rows":[{"nodeRef":-1188832417,"type":"node","props":{"name":"Math","uuid":"3a839cc1-9af7-45fd-a29c-bfe96705655e","dateAdded":"17-06-14"}}]},"resultDataContents":["row"],"includeStats":false}]}
2017-06-14 09:48:26.995 INFO 6788 --- [nio-8083-exec-7] o.n.o.drivers.http.request.HttpRequest : Thread: 24, url: http://localhost:7474/db/data/transaction/31, request: {"statements":[{"statement":"UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId MATCH (endNode) WHERE ID(endNode) = row.endNodeId MERGE (startNode)-[rel:`parent`]->(endNode) RETURN row.relRef as ref, ID(rel) as id, row.type as type","parameters":{"rows":[{"startNodeId":7,"relRef":-240693881,"type":"rel","endNodeId":6}]},"resultDataContents":["row"],"includeStats":false},{"statement":"UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId MATCH (endNode) WHERE ID(endNode) = row.endNodeId MERGE (startNode)-[rel:`children`]->(endNode) RETURN row.relRef as ref, ID(rel) as id, row.type as type","parameters":{"rows":[{"startNodeId":6,"relRef":-12081903,"type":"rel","endNodeId":7}]},"resultDataContents":["row"],"includeStats":false}]}
2017-06-14 09:48:27.288 INFO 6788 --- [nio-8083-exec-7] o.n.o.drivers.http.request.HttpRequest : Thread: 24, url: http://localhost:7474/db/data/transaction/32, request: {"statements":[{"statement":"MATCH (n:`Category`) WITH n MATCH p=(n)-[*0..1]-(m) RETURN p","parameters":{},"resultDataContents":["graph"],"includeStats":false}]}
2017-06-14 09:48:27.344 DEBUG 6788 --- [nio-8083-exec-7] o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@241797fe
Create Biology subcategory of Non-fiction parent category
2017-06-14 09:51:12.367 DEBUG 6788 --- [nio-8083-exec-1] o.s.b.w.f.OrderedRequestContextFilter : Bound request context to thread: org.apache.catalina.connector.RequestFacade@241797fe
2017-06-14 09:51:12.385 INFO 6788 --- [nio-8083-exec-1] o.n.o.drivers.http.request.HttpRequest : Thread: 18, url: http://neo4j:password@localhost:7474/db/data/transaction/commit, request: {"statements":[{"statement":"MATCH (n:`Category`) WHERE n.`name` = { `name_0` } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p, ID(n)","parameters":{"name_0":"Biology"},"resultDataContents":["graph","row"],"includeStats":false}]}
2017-06-14 09:51:12.394 INFO 6788 --- [nio-8083-exec-1] o.n.o.drivers.http.request.HttpRequest : Thread: 18, url: http://localhost:7474/db/data/transaction/37, request: {"statements":[{"statement":"MATCH (n) WHERE n.uuid = { id } WITH n MATCH p=(n)-[*0..1]-(m) RETURN p","parameters":{"id":"8004d142-85c5-461b-862e-aee7ddfc90fa"},"resultDataContents":["graph"],"includeStats":false}]}
2017-06-14 09:51:12.405 INFO 6788 --- [nio-8083-exec-1] o.n.o.drivers.http.request.HttpRequest : Thread: 18, url: http://localhost:7474/db/data/transaction/38, request: {"statements":[{"statement":"UNWIND {rows} as row MERGE (n:`Category`{uuid: row.props.uuid}) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, row.type as type","parameters":{"rows":[{"nodeRef":-1174392366,"type":"node","props":{"name":"Biology","uuid":"8004d142-85c5-461b-862e-aee7ddfc90fa","dateAdded":"17-06-14"}}]},"resultDataContents":["row"],"includeStats":false}]}
2017-06-14 09:51:12.414 INFO 6788 --- [nio-8083-exec-1] o.n.o.drivers.http.request.HttpRequest : Thread: 18, url: http://localhost:7474/db/data/transaction/38, request: {"statements":[{"statement":"UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId MATCH (endNode) WHERE ID(endNode) = row.endNodeId MERGE (startNode)-[rel:`parent`]->(endNode) RETURN row.relRef as ref, ID(rel) as id, row.type as type","parameters":{"rows":[{"startNodeId":8,"relRef":-1580985829,"type":"rel","endNodeId":6}]},"resultDataContents":["row"],"includeStats":false},{"statement":"UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId MATCH (endNode) WHERE ID(endNode) = row.endNodeId MERGE (startNode)-[rel:`children`]->(endNode) RETURN row.relRef as ref, ID(rel) as id, row.type as type","parameters":{"rows":[{"startNodeId":7,"relRef":-2084880007,"type":"rel","endNodeId":6},{"startNodeId":6,"relRef":-38658551,"type":"rel","endNodeId":8}]},"resultDataContents":["row"],"includeStats":false}]}
2017-06-14 09:51:12.456 INFO 6788 --- [nio-8083-exec-1] o.n.o.drivers.http.request.HttpRequest : Thread: 18, url: http://localhost:7474/db/data/transaction/39, request: {"statements":[{"statement":"MATCH (n:`Category`) WITH n MATCH p=(n)-[*0..1]-(m) RETURN p","parameters":{},"resultDataContents":["graph"],"includeStats":false}]}
2017-06-14 09:51:12.520 DEBUG 6788 --- [nio-8083-exec-1] o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@241797fe
My code which create the new subcategory is the following:
@PostMapping("/admin/category/newMid")
String newMidCategory(Model m, @RequestParam("newMidCategory") String newMidCategoryName,
@RequestParam("selectedMainCategory") Category mainCategory) {
if (mainCategory != null) {
Category existing = categoryService.findCategoryByName(newMidCategoryName);
if (existing == null) {
// indeed it is new category
Category newMidCategory = new Category(newMidCategoryName, mainCategory);
mainCategory.addChildren(newMidCategory);
categoryService.insertNewCategory(newMidCategory);
} else {
m.addAttribute("midCatError", ctx.getMessage("error.admin.MidCategoryExistsAlready", null,
new Locale(env.getProperty("spring.mvc.locale"))));
}
} else {
m.addAttribute("midCatError", ctx.getMessage("error.admin.mainCategoryNotSelected", null,
new Locale(env.getProperty("spring.mvc.locale"))));
}
m.addAttribute("midCategories", categoryService.getChildrenOfParent(mainCategory));
m.addAttribute("allMainCategories", categoryService.getAllMainCategories());
return "admin/category :: #categoryForm";
}
After several hours of debugging it turned out that somehow the subcategory (Math) children set has a member the non fiction main category so the OGM just maps it correctly.
The question is how can it possible that my private children property can be modified without calling the public methods addChildren(Category c). There is a System.out.println() line in my addChildren method to see when it is called. It is called only two time: first when we add Math subcategory, second when we add Biology subCategory. Without the addChildren invocation Math subcategory has a children? What is this?
This looks like a bug.
You could try to annotate also the setters for the relationship fields as a workaround:
@Relationship(type = "parent", direction = Relationship.OUTGOING)
private Category parent;
@Relationship(type = "children", direction = Relationship.OUTGOING)
private Set<Category> children;
@Relationship(type = "parent", direction = Relationship.OUTGOING)
public void setParent(Category parent) {
this.parent = parent;
}
@Relationship(type = "children", direction = Relationship.OUTGOING)
public void setChildren(Set<Category> children) {
this.children = children;
}
If you could reproduce it using SDN/OGM issue template it would be great. https://github.com/neo4j-examples/neo4j-sdn-ogm-issue-report-template