1
votes

I'm using Spring Data Neo4J 5.0.10 with Spring Boot 2.0.5. I have the following 2 node entities, user interest and a relationship entity user interest.

@NodeEntity
public class User {

    private Long id;    

    @Id 
    @GeneratedValue(strategy = UserIdStrategy.class)
    @Convert(UuidStringConverter.class)
    private UUID userId;

    @Relationship(type = UserInterest.TYPE, direction = Relationship.OUTGOING)
    private Set<UserInterest> interests = new HashSet<>();

    ... getters/setters

@NodeEntity
public class Interest {

    private Long id;

    @Id 
    @GeneratedValue(strategy = InterestIdStrategy.class)
    private String interestId;

    private String name;

    ... getters/setters

@RelationshipEntity(type = UserInterest.TYPE)
public class UserInterest {

    public static final String TYPE = "INTERESTED_IN";

    private Long id;

    @StartNode
    private User start;

    @EndNode
    private Interest end;

    //private Long weight;

    ... getters/setters

This works great. I can create a new User and associate the user to the userInterest. When I send the same details again, the nodes and edges are not duplicated.

When I enable the weight attribute in the relationship entity, it seems that the relationship is duplicated even though the weight attribute value is the same.

I recall reading that as along as the attributes are the same, another relationship should not be created, is that correct?

Is this is expected behaviour what do I need to do to prevent duplicating the relationship?

1
Can you please share how you are saving the entities. Also when you have created a relationship class for nodes then you don't need to add the userInterest to the user class. - Vipul Pandey

1 Answers

3
votes

Here's a working solution. Before I go into details: The key is what things your persisting. You should aim for a clear bounded context and just access interest to one aggregate. I decided for the user being the entry point to things. The user has interests and interests should be added and manipulated through the user.

The OGM and Spring Data Neo4j takes care of saving relationships outgoing from the user.

So the main points are: Don't save every NodeEntity yourself. Save associations between entities in an implicit way, that is: Save the parent object only. You can do this through the session itself or as I did it, through a repository. Take note that you don't need a repository for each and every entity.

I have omitted the custom strategies as you didn't share them. I'm relying on the generated Ids. If my example fails with your strategies, maybe it's a good hint where to look for bugs.

We have the interest:

@NodeEntity
public class Interest {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

And the users interest:

@RelationshipEntity(type = UserInterest.TYPE)
public class UserInterest {

    public static final String TYPE = "INTERESTED_IN";

    private Long id;

    @StartNode
    private User start;

    @EndNode
    private Interest end;

    private Long weight;

    public void setStart(User start) {
        this.start = start;
    }

    public Interest getEnd() {
        return end;
    }

    public void setEnd(Interest end) {
        this.end = end;
    }

    public void setWeight(Long weight) {
        this.weight = weight;
    }
}

And finally the user:

public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @Relationship(type = UserInterest.TYPE, direction = Relationship.OUTGOING)
    private Set<UserInterest> interests = new HashSet<>();

    public void setName(String name) {
        this.name = name;
    }

    public Interest setInterest(String interstName, long weight) {

        final UserInterest userInterest = this.interests.stream()
            .filter(i -> interstName.equalsIgnoreCase(i.getEnd().getName()))
            .findFirst()
            .orElseGet(() -> {
                // Create a new interest for the user
                Interest interest = new Interest();
                interest.setName(interstName);

                // add it here to the interests of this user
                UserInterest newUserInterest = new UserInterest();
                newUserInterest.setStart(this);
                newUserInterest.setEnd(interest);
                this.interests.add(newUserInterest);
                return newUserInterest;
            });
        userInterest.setWeight(weight);
        return userInterest.getEnd();
    }
}

See setInterest. This is one way to use the User as the aggregate root to access all the things. Here: The interest. If it exists, just modify the weight, otherwise create a new one, including UserInterest, add it to the users interests, finally set the weight and then return it for further use.

Then, I declare one repository, for the user only:

public interface UserRepository extends Neo4jRepository<User, Long> {
    Optional<User> findByName(String name);
}

And now the application:

@SpringBootApplication
public class SorelationshipsApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(SorelationshipsApplication.class, args);
    }

    private final UserRepository userRepository;

    private final SessionFactory sessionFactory;

    public SorelationshipsApplication(UserRepository userRepository, SessionFactory sessionFactory) {
        this.userRepository = userRepository;
        this.sessionFactory = sessionFactory;
    }

    @Override
    public void run(String... args) throws Exception {
        Optional<User> optionalUser = this.userRepository
            .findByName("Michael");
        User user;

        ThreadLocalRandom random = ThreadLocalRandom.current();
        if(optionalUser.isPresent()) {

            // Redefine interests and add a new one
            user = optionalUser.get();
            user.setInterest("Family", random.nextLong(100));
            user.setInterest("Bikes", random.nextLong(100));
            user.setInterest("Music", random.nextLong(100));
        } else {
            user = new User();
            user.setName("Michael");

            user.setInterest("Bikes", random.nextLong(100));
            user.setInterest("Music", random.nextLong(100));

        }

        userRepository.save(user);
        // As an alternative, this works as well...
        // sessionFactory.openSession().save(user);
    }
}

It's only a command line example running against my local Neo4j instance, but I think it explains things well enough.

I check if a user exists. If not, create it and add some interest. On the next run, modify existing interests and create a new one. Any further run just modifies existing interests.

See the result:

User and interests

Add bonus: If you're on Java 11, see ifPresentOrElse on Optional. Much more idiomatic way of dealing with Optionals.

userRepository.findByName("Michael").ifPresentOrElse(existingUser -> {
    existingUser.setInterest("Family", random.nextLong(100));
    existingUser.setInterest("Bikes", random.nextLong(100));
    existingUser.setInterest("Music", random.nextLong(100));
    userRepository.save(existingUser);
}, () -> {
    User user = new User();
    user.setName("Michael");

    user.setInterest("Bikes", random.nextLong(100));
    user.setInterest("Music", random.nextLong(100));
    userRepository.save(user);
});

I hope that helps.

Edit: Here are my dependencies:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>sorelationships</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sorelationships</name>
    <description>Demo project for Spring Boot</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-neo4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>