Prevent Multiple Relationships between two nodes in Neo4J - neo4j

I am building an application using Spring-Data-Neo4J (4.0.0.RELEASE). The application has two entities, a Person entity, and a Group entity. There is also a relationship entity, Member_Of, which says that a person is a member of a group. The entities look like below
#NodeEntity(label="Person")
public class Person {
protected String uuid;
protected String fullName;
protected String email;
#Relationship(type = RelationshipNames.MEMBER_OF, direction = Relationship.OUTGOING)
protected List<GroupMembership> groupMemberships = new ArrayList<GroupMembership>() ;
}
#NodeEntity(label="Group")
public class Group implements Serializable{
protected static final long serialVersionUID = 1L;
#GraphId
protected Long id;
protected String uuid;
protected String name;
#Relationship(type = RelationshipNames.MEMBER_OF, direction = Relationship.INCOMING)
protected List<GroupMembership> groupMemberships = new ArrayList<GroupMembership>() ;
}
#RelationshipEntity(type = RelationshipNames.MEMBER_OF)
public class GroupMembership implements Serializable{
private static final long serialVersionUID = 1L;
#GraphId Long id;
#Property String uuid;
#StartNode Person member;
#EndNode Group group;
#DateLong
Date date;
}
There is a method which adds members to a group. The method is annotated with #Transactional. The sample code is given below.
#Override
#Transactional(propagation=Propagation.REQUIRED)
public ResponseEntity<GroupVO> addMembersToGroup(String groupUuid, String personUuid, List<String> personsToAdd){
Group group = groupRepository.findGroupByUuid(groupUuid);
List<Person> personsToAdd = IterableUtils.toList(personRepository.findPersonsByUuid(personUuids));
personsToAdd.forEach(personToAdd -> {
GroupMembership existingGroupMembership = getActiveMembership(group.getUuid(), personToAdd.getUuid());
if(existingGroupMembership==null){
GroupMembership newGroupMembership = new GroupMembership(personToAdd, group, GroupRoleNames.MEMBER, member.getUuid(), new Date());
personToAdd.getGroupMemberships().add(newGroupMembership);
group.getGroupMemberships().add(newGroupMembership);
}
groupRepository.save(group);
}
What it tries to do is that, it searches for a existing relationship between the personToAdd and the group. If it returns null, that is, no relationship exists, it adds it to the group.
The problem is sometimes the same person is added multiple times to the same group. This is happenning when two people are running the application and both of them tries to add the same person to the same group.
How do I prevent this from happenning? I need to have a single relationship between a person and the group and not multiple ones.

There will be only one relationship created between two given entities provided all properties on the relationship entity are equal. You have a timestamp which is the culprit here- SDN realises that the two relationships differ because they have different values for a property and goes ahead and creates the second one.
At the moment, SDN does not have configuration to allow you to specify a merge vs create for relationship entities.
You'll probably have to manage some synchronization at the application level.

I had the same issue, and was able to "solve" it by using a custom cypher query. Given that you have added successfully Person and Group entity you can run the following query:
#Query("MATCH (group:Group) " +
"MATCH (person:Person) " +
"WHERE person.uuid={0} AND group.uuid={1} "+
"MERGE (person)-[r:MEMBER_OF]->(group) " +
"SET r.uuid ={2} , r.date={3} " +
"RETURN r")
GroupMembership isMemberOf(String personUuid,String groupUuid, String uuid, Date date);
by calling it like this:
personRepository.isMemberOf(personUuid,groupUuid,uuid,date);
However, don't take it for granted. I haven't done extensive tests to ensure that this approach is thread-safe.
This answer by William Lyon on atomic execution of MERGE may be an extra step that you have to take.

Related

Spring Neo4j - Bolt driver fails to load a relationship while the Http driver does it successfully

While coding a poc with Spring Neo4j using spring boot I came across what it seems to be an inconsistent behavior between the Bolt driver and the Http driver. Basically after saving a rich relationship between 2 nodes the test fails to load it when using the Bolt driver, however the exact same test succeeds when trying with the Http driver.
The sample project can be downloaded from github
It's a very basic/straight forward test, the only pre-requisite is that you will need to have Neo4j 3 installed with the Bolt connector enabled.
As suggested by Andrej please find below the relevant sections of the code:
#NodeEntity(label = "Person")
public class Person {
private Long id;
private String firstname;
private String lastname;
#Relationship(type = "HAS_CONTACT", direction = Relationship.INCOMING)
private Contact contact;
// getters and setters here .........
}
#NodeEntity(label = "BankAccount")
public class BankAccount {
private Long id;
private Integer balance;
#Relationship(type = "HAS_CONTACT")
private List<Contact> contacts = new ArrayList<>();
// getters and setters here .........
}
#RelationshipEntity(type = "HAS_CONTACT")
public class Contact {
public Contact() {
}
public Contact(BankAccount bankAccount, Person person) {
this.bankAccount = bankAccount;
this.person = person;
this.bankAccount.getContacts().add(this);
this.person.setContact(this);
}
private Long id;
#StartNode
private BankAccount bankAccount;
#EndNode
private Person person;
private String email;
private String phoneNumber;
// getters and setters here .........
}
#Repository
public interface ContactRepository extends GraphRepository<Contact> {
#Query("MATCH (a:BankAccount)-[r:HAS_CONTACT]->(:Person) " +
"WHERE ID(a)={accountId} " +
"RETURN r")
Iterable<Contact> findByAccountId(#Param("accountId") Long accountId);
}
After saving 1 account, 1 person and 1 contact relationship between them the below query is the one that fails:
Iterable<Contact> contacts = contactRepository.findByAccountId(accountId);
// this assertion will Fail for the BOLT driver, however, it will Succeed for the HTTP driver
// if the accountRepository.findOne(accountId) statement is executed before calling
// contactRepository.findByAccountId(accountId) then the test will also succeed for the BOLT driver
assertThat(size(contacts), is(1));
See below the response from the neo4j team about the issue:
The reason the relationship entity cannot be mapped is because the start and end node are unavailable with this query:
#Query("MATCH (a:BankAccount)-[r:HAS_CONTACT]->(p:Person) " +
"WHERE ID(a)={accountId} " +
"RETURN r,")
Iterable<Contact> findByAccountId(#Param("accountId") Long accountId);
You need to return these nodes to be able to construct a valid
relationship entity, like this:
#Query("MATCH (a:BankAccount)-[r:HAS_CONTACT]->(p:Person) " +
"WHERE ID(a)={accountId} " +
"RETURN r,a,p")
Iterable<Contact> findByAccountId(#Param("accountId") Long accountId);
It's a side effect of the HTTP endpoint that allows this to pass- the
start and end node are returned by this endpoint anyway. Please see
http://graphaware.com/neo4j/2016/04/06/mapping-query-entities-sdn.html
for more info.

How to save and retrieve nested objects in neo4j using spring data

I have a model where User will have a list of Roles and Role has a list of Permissions. However, even when i save all of them at once - with depth -1 I am unable to retrieve the child nodes from the parent nodes.
ex: user.getRoles() - 2 [role1,role2]
role1.getAssociatedFeature() - 0
But if i get the Role from the DB
Ex : findByRoleName('role1') -> [Role: role1,Display Role,associatedFeatures[2]]
User.java
#NodeEntity
public class User {
#GraphId Long id;
private String name;
private String loginUserName;
#Relationship(type="ROLE")
private Set<Role> associatedRoles = new HashSet<Role>();
}
Role.java
#NodeEntity
public class Role {
#GraphId Long id;
private String roleName;
private String displayRoleName;
#Relationship(type="ACCESS_TO")
private Set<Feature> associatedFeatures = new HashSet<Feature>();
}
Feature.java
#NodeEntity
public class Feature {
#GraphId Long id;
private String featureName;
#Relationship(type="HAS_PERMISSION")
private Set<Permission> permissions = new HashSet<Permission>();
}
#NodeEntity
public #Data class Permission {
#GraphId
Long id;
String permission;
}
I am using Spring data jpa to use the CRUD operations:
<>Repository.java - This will bydefault implement save,update,delete,find
#RepositoryRestResource()
public interface RoleRepository extends GraphRepository<Role>{...}
ServiceImpl.java
#Override
public User create(User u) {
return userRepo.save(u,-1);
}
In my Junit- I am creating a new User entity, and populating the data all the way to permission. But when i fetch the user -> i only get the roles but not the features, permission along the chain.
In the neo4j DB browser, I see that all the nodes are created with appropriate dependency. Any pointers on how to save and traverse through the graph?
The default load depth is 1. This means you'll get the user and the associated roles, but not the role's features or anything deeper in the graph.
You can specify the load depth if the default is not what you want:
userRepo.findOne(user.getId(), 3);
http://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#_fine_grained_control_via_depth_specification

How to get the direct relationship entities and directly related nodes in custom query in SDN4?

I have an annotated finder method in my repository:
#Query("MATCH (me:User)<-[ab:ASKED_BY]-(q:Question) WHERE id(me) = {0} RETURN q")
Iterable<Question> findQuestionsByUserId(Long id);
My objects like:
#NodeEntity
public class Question {
private AskedBy askedBy;
#Relationship(type = "TAGGED_WITH")
private Set<Tag> tags = new HashSet<>();
//...
}
#RelationshipEntity(type = "ASKED_BY")
public class AskedBy {
#GraphId private Long id;
#StartNode
private User user;
#EndNode
private Question question;
// other props
}
When I call the repository method, the askedBy field is null in the result. How can I populate that field with the relationship?
Update:
I have tried to load the relationship with session loadAll(collection) but it did not help.
final Collection<Question> questions = (Collection<Question>) questionRepository.findQuestionsByUserId(user.getId());
final Question q = questions.iterator().next();
System.out.println("After `findQuestionsByUserId`:");
System.out.println("`q.getTags().size()`: " + q.getTags().size());
System.out.println("`q.getAskedBy()`: " + q.getAskedBy());
neo4jOperations.loadAll(questions, 1);
System.out.println("After `neo4jOperations.loadAll(questions, 1)`:");
System.out.println("`q.getTags().size()`: " + q.getTags().size());
System.out.println("`q.getAskedBy()`: " + q.getAskedBy());
final Collection<AskedBy> askedByCollection = neo4jOperations.loadAll(AskedBy.class);
System.out.println("`askedByCollection.size()`: " + askedByCollection.size());
The above snippet outputs
After findQuestionsByUserId:
q.getTags().size(): 0
q.getAskedBy(): null
After neo4jOperations.loadAll(questions, 1):
q.getTags().size(): 1
q.getAskedBy(): null
askedByCollection.size(): 0
So it seems the default depth is 0 for the custom query, and for some unknown reason I can not load the relationship entity.
The graph looks okay:
At the moment, custom queries do not support a depth parameter (it's on the roadmap), so you have the following options-
a) Use repository.findOne(userId) (the default is depth 1 so it should load AskedBy). Or customize the depth with repository.findOne(userId,depth). Or use Neo4jTemplate.load(type,id,depth)
b) If you need to query on more than the id, use the loadAll methods on the org.neo4j.ogm.session.Session that accept a set of org.neo4j.ogm.cypher.Filter. Examples available in MusicIntegrationTest
c) Continue with the custom query but after you get the entity ID back, load it via the load* methods providing a custom depth.

SDN4 or neo4j-ogm performances issue

I wrote some simple java code and I encountered some bad performances with SDN4 that I didn't have with SDN3. I suspect the find repositories methods depth parameter to not work exactly in the way it should be. Let me explain the problem:
Here are my java classes(it's just an example) in which I removed getters, setters, contructors, ...
First class is 'Element' :
#NodeEntity
public class Element {
#GraphId
private Long id;
private int age;
private String uuid;
#Relationship(type = "HAS_VALUE", direction = Relationship.OUTGOING)
private Set<Value> values = new HashSet<Value>();
Second one is 'Attribute'
#NodeEntity
public class Attribute {
#GraphId
private Long id;
#Relationship(type = "HAS_PROPERTIES", direction = Relationship.OUTGOING)
private Set<HasInterProperties> properties;
The 'value' class allow my user to add a value on an Element for a specific attribute :
#RelationshipEntity(type = "HAS_VALUE")
public class Value {
#GraphId
private Long id;
#StartNode
Element element;
#EndNode
Attribute attribute;
private Integer value;
private String uuid;
public Value() {
}
public Value(Element element, Attribute attribute, Integer value) {
this.element = element;
this.attribute = attribute;
this.value = value;
this.element.getValues().add(this);
this.uuid = UUID.randomUUID().toString();
}
'Element' classe really need to know its values but 'Attribute' class do not care at all about values.
An attribute has references on InternationalizedProperties class which is like that :
#NodeEntity
public class InternationalizedProperties {
#GraphId
private Long id;
private String name;
The relationship entity between an attribute and it InternationalizedProperties is like the following :
#RelationshipEntity(type = "HAS_PROPERTIES")
public class HasInterProperties {
#GraphId
private Long id;
#StartNode
private Attribute attribute;
#EndNode
private InternationalizedProperties properties;
private String locale;
I then created a little main method to create two attributes and 10000 elements. All my elements have a specific value for the first attribute but no values for the second one (no relation between them). Both attributes hav two differents internationalizedProperties. Here is a sample :
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring/*.xml");
Session session = context.getBean(Session.class);
session.query("START n=node(*) OPTIONAL MATCH n-[r]-() WHERE ID(n) <> 0 DELETE n,r", new HashMap<String, Object>());
ElementRepository elementRepository = context.getBean(ElementRepository.class);
AttributeRepository attributeRepository = context.getBean(AttributeRepository.class);
InternationalizedPropertiesRepository internationalizedPropertiesRepository = context.getBean(InternationalizedPropertiesRepository.class);
HasInterPropertiesRepository hasInterPropertiesRepository = context.getBean(HasInterPropertiesRepository.class);
//Creation of an attribute object with two internationalized properties
Attribute att = new Attribute();
attributeRepository.save(att);
InternationalizedProperties p1 = new InternationalizedProperties();
p1.setName("bonjour");
internationalizedPropertiesRepository.save(p1);
InternationalizedProperties p2 = new InternationalizedProperties();
p2.setName("hello");
internationalizedPropertiesRepository.save(p2);
hasInterPropertiesRepository.save(new HasInterProperties(att, p1, "fr"));
hasInterPropertiesRepository.save(new HasInterProperties(att, p2, "en"));
LOGGER.info("First attribut id is {}", att.getId());
//Creation of 1000 elements having a differnt value on a same attribute
for(int i = 0; i< 10000; i++) {
Element elt = new Element();
new Value(elt, att, i);
elementRepository.save(elt);
if(i%50 == 0) {
LOGGER.info("{} elements created. Last element created with id {}", i+1, elt.getId());
}
}
//Another attribut without any values from element.
Attribute att2 = new Attribute();
attributeRepository.save(att2);
InternationalizedProperties p12 = new InternationalizedProperties();
p12.setName("bonjour");
internationalizedPropertiesRepository.save(p12);
InternationalizedProperties p22 = new InternationalizedProperties();
p22.setName("hello");
internationalizedPropertiesRepository.save(p22);
hasInterPropertiesRepository.save(new HasInterProperties(att2, p12, "fr"));
hasInterPropertiesRepository.save(new HasInterProperties(att2, p22, "en"));
LOGGER.info("Second attribut id is {}", att2.getId());
Finally, in another main method, I try to get several times the first attribute and the second one :
private static void getFirstAttribute(AttributeRepository attributeRepository) {
StopWatch st = new StopWatch();
st.start();
Attribute attribute = attributeRepository.findOne(25283L, 1);
LOGGER.info("time to get attribute (some element have values on it) is {}ms", st.getTime());
}
private static void getSecondAttribute(AttributeRepository attributeRepository) {
StopWatch st = new StopWatch();
st.start();
Attribute attribute2 = attributeRepository.findOne(26286L, 1);
LOGGER.info("time to get attribute (no element have values on it) is {}ms", st.getTime());
}
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring/*.xml");
AttributeRepository attributeRepository = context.getBean(AttributeRepository.class);
getFirstAttribute(attributeRepository);
getSecondAttribute(attributeRepository);
getFirstAttribute(attributeRepository);
getSecondAttribute(attributeRepository);
getFirstAttribute(attributeRepository);
getSecondAttribute(attributeRepository);
getFirstAttribute(attributeRepository);
getSecondAttribute(attributeRepository);
}
Here are the logs of this execution :
time to get attribute (some element have values on it) is 2983ms
time to get attribute (no element have values on it) is 4ms
time to get attribute (some element have values on it) is 1196ms
time to get attribute (no element have values on it) is 2ms
time to get attribute (some element have values on it) is 1192ms
time to get attribute (no element have values on it) is 3ms
time to get attribute (some element have values on it) is 1194ms
time to get attribute (no element have values on it) is 3ms
Getting the second attribut (and its internationalized properties thanks to depth=1) is very quick but to get the first one remains very slow. I know that there are many relations (10000 exactly) which are pointing on the first attribute, but when I want to get an attribute with its internationalized properties I clearly do not want to get all the values which are pointing on it. (since Set is not specified on Attribute class).
That's why I think there is a performance problem here. Or may be I do something wrong ?
Thanks for your help
When loading data from the graph we don't currently analyse how your domain model is wired together, so we may potentially bring back related nodes that you do not require. These will then be discarded if they are not mappable in your domain, but if there are many of them, it could potentially impact response times.
There are two reasons for this approach.
It is obviously much simpler to create generic queries to any depth,than it would be dynamically analyse your domain model to any arbitrary depth and generate on-the-fly custom queries; its also much simpler to analyse and prove the correctness of generic queries.
We want to preserve the capability to support polymorphic domain
models in the future, where we don't necessarily know what's in the
database from one day to the next, but we want to adapt our domain
model hydration according to what we find.
In this case I would suggest writing a custom query to load the Attribute objects, to ensure you don't bring back all the unwanted relationships.

Issues while retrieving existing nodes using Spring Data Neo4j

I created a simple SDN project to retrieve existing nodes from my database. In the repository, I defined a custom query using #Query annotation which is something like
#Query("MATCH (EMP:EMPLOYEE) WHERE EMP.empName={0} return EMP")
public Employee findByName(String empName);
#RelationshipEntity(type = "HAS_ADDRESS")
class AddressRelationShip
{
#GraphId
Long id;
#StartNode
Employee employee = null;
#EndNode
Address address = null;
public AddressRelationShip(Employee employee, Address address)
{
this.employee = employee;
this.address = address;
}
}
#NodeEntity
#TypeAlias("EMPLOYEE")
public class Employee
{
#GraphId
Long id;
String empName = null;
#RelatedTo(type = "HAS_ADDRESS", direction = Direction.OUTGOING)
#Fetch
Set<Address> addresses;
public void addressEmplployee(Address address)
{
if (addresses == null)
{
addresses = new HashSet<Address>();
}
//AddressRelationShip addressRelationShip = new AddressRelationShip(this, address);
addresses.add(address);
}
public Set<Address> getAddresses()
{
return addresses;
}
public void setAddresses(Set<Address> addresses)
{
this.addresses = addresses;
}
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public String getEmpName()
{
return empName;
}
public void setEmpName(String empName)
{
this.empName = empName;
}
}
With this query, on execution I get the below error message:
No primary SDN label exists .. (i.e one starting with _)
I googled about the issue and tried to use the below query:
MATCH (EMP:EMPLOYEE:_EMPLOYEE) WHERE EMP.EmployeeId={0} return EMP
This query runs but it doesn't return any response.
One important thing here is that I haven't created the existing nodes using SDN(I googled and found that SDN adds some metadata e.g, _ to nodes/relationships).
However, if I created the (Employee)-[HAS_ADDRESS]->(ADDRESS) pattern data using SDN, below query works fine:
MATCH (EMP:EMPLOYEE) WHERE EMP.empName={0} return EMP
In this case, I found one other issue that it returned address data as well while I'm only returning Employee in the query.
I'm able to obtain the addresses from the Employee entity object.
Any pointers on the above issues?
PS - Neo4j is running in standalone server mode.
Regards,
Rahul
I could resolve the above issues in following steps:
No primary SDN label exists .. (i.e one starting with _) - In SDN 3.3.0, for existing nodes, SDN requires an extra label (in my case, _EMPLOYEE), so a data migration is required. In SDN 4.0, It seems that this is no longer needed but I haven't tried 4.0 yet.
Returning address data as well while I'm only returning Employee in the query - Removing #Fetch on Set addresses in Employee resolved this, However, addresses nodeIds were still returned.
To run SDN 3.x.x with existing data, following data migration is required:
Add additional NodeLabel(preceding the original label with _) to nodes, e.g, add _Employee label to all Employee nodes.
Add __type__ property to nodes and relationships whose value will be fully qualified name for the appropriate domain/model classes, e.g,
match (n:Employee) set n.__type__="org.neo4j.domain.Employee"
Cheers,
Rahul

Resources