Search code examples
spring-mvcspring-data-neo4j-4

Spring Data Neo4j inserts unexpected relations (edges) during save


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?


Solution

  • 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