Search code examples
neo4jspring-dataspring-data-neo4j

How can I use Spring Data Neo4j to create a Node with new Relationships to existing Nodes


My use case is that I want to send a Message from one User to another, by creating a Message node in between two Users. Here is the JSON I would like to send from the client, and my Message class:

{"text":"Hello!","recipient":{"userId":"1823498e-e491-45fa-95bb-782a937a00ba"},"sent":{"userId":"6467b976-7ff6-4817-98e0-f3fbfca1413e"}}
@Node("Message")
@Data
public class Message
{   
    @Id @GeneratedValue private UUID messageId; 
    
    @NotNull    
    private String text;
    
    @Relationship(type = "SENT",direction = Direction.INCOMING) 
    private User sent;
    
    @Relationship("TO") 
    private User recipient;
}

But when I call Mono<Message> save(Message message); in the ReactiveNeo4jRepository, the DB unique constraint complains that Users with those usernames already exist! It seems like it's trying to reset all of the other properties for those two userIds to empty/defaults, presumably since they're not included in the JSON. So I get a constraint error when it sees two "FAKE" usernames.

2022-03-26 14:34:29.120 DEBUG 51863 --- [o4jDriverIO-2-4] o.n.d.i.a.o.OutboundMessageHandler       : [0xb08eba68][x.server.com:7687][bolt-128] C: RUN "MERGE (user:`User` {userId: $__id__}) SET user += $__properties__ RETURN user" {__id__="6467b976-7ff6-4817-98e0-f3fbfca1413e", __properties__={lastName: NULL, credentialsNonExpired: FALSE,userId: "6467b976-7ff6-4817-98e0-f3fbfca1413e", enabled: FALSE, firstName: NULL, password: "FAKE",accountNonExpired: FALSE, email: NULL,username: "FAKE", accountNonLocked: FALSE}} {bookmarks=["FB:kcwQx2Hl5+KYQJiGdiyzOa4EWSCQ"]}
2022-03-26 14:34:29.206 DEBUG 51863 --- [o4jDriverIO-2-4] o.n.d.i.a.i.InboundMessageDispatcher     : [0xb08eba68][x.server.com:7687][bolt-128] S: SUCCESS {fields=["user"], t_first=2}
2022-03-26 14:34:29.207 DEBUG 51863 --- [o4jDriverIO-2-4] o.n.d.i.a.o.OutboundMessageHandler       : [0xb08eba68][x.server.com:7687][bolt-128] C: PULL {n=-1}
2022-03-26 14:34:29.301 DEBUG 51863 --- [o4jDriverIO-2-4] o.n.d.i.a.i.InboundMessageDispatcher     : [0xb08eba68][x.server.com:7687][bolt-128] S: FAILURE Neo.ClientError.Schema.ConstraintValidationFailed "Node(1) already exists with label `User` and property `username` = 'FAKE'"

UPDATE: I can accomplish what I need to using an ObjectMapper from Jackson and the Neo4j Driver directly, as follows. But I still would like to know how to this with SDN.

    @Override
    public Mono<UUID> save(Message message) 
    {
        String cypher = "MATCH (a:User),(b:User) where a.userId = $fromUserId and b.userId = $toUserId CREATE (a)-[:SENT]->(m:Message {messageId: $messageId, text : $text})-[:TO]->(b)";
        Map<String,Object> objMap = persistenceObjectMapper.convertValue(message,mapType);
        return Mono.from(driver.rxSession().writeTransaction(tx -> tx.run(cypher,objMap).records())).then(Mono.just(message.getMessageId()));
    }

Solution

  • It seems like it's trying to reset all of the other properties for those two userIds to empty/defaults

    The problem you are facing is rooted in the handling of the incoming Message object. As far as I understood this, you are directly saving the Message with the attached, de-serialized Users.

    You have to fetch the Users from the database first. SDN does no pre-fetch. One way to do this is to create an additional repository for the User entity.

    Given a MovieService that gets called by the controller, you would end up with something like this:

    @Transactional
    public Message saveNewMessage(Message newMessage) {
        UUID recipientId = newMessage.getRecipient().getUserId();
        UUID senderId = newMessage.getSent().getUserId();
    
        User recipient = userRepository.findById(recipientId).orElseGet(() -> {
            User user = new User();
            user.setName("new recipient");
            return user;
        });
    
        User sender = userRepository.findById(senderId).orElseGet(() -> {
            User user = new User();
            user.setName("new sender");
            return user;
        });
    
        newMessage.setRecipient(recipient);
        newMessage.setSent(sender);
    
        return messageRepository.save(newMessage);
    }
    

    Tested this with:

    CREATE CONSTRAINT username ON (user:User) ASSERT user.name IS UNIQUE
    

    for 4.3 and/or for 4.4

    CREATE CONSTRAINT username FOR (user:User) REQUIRE user.name IS UNIQUE
    

    I created an example for your use case: https://github.com/meistermeier/neo4j-issues-examples/tree/master/so-71594275 There I also a user properties erasing call stack (/clearusers) that shows what I think happens in your application.

    Edit: Since I usually advice not to create a repository for every entity class, you could also use the Neo4jTemplate. I have added the saveNewMessageAlternative to the example.