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()));
}
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 User
s.
You have to fetch the User
s 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.